From b7cfce2b0a63fc54941ff3d5fe317f514e1649d6 Mon Sep 17 00:00:00 2001 From: Aleksandr Trushev Date: Tue, 19 Dec 2023 16:27:35 +0000 Subject: [PATCH] IJPL-440 Support editor markup cache for declarative inlay hints Merge-request: IJ-MR-122603 Merged-by: Aleksandr Trushev GitOrigin-RevId: 9e60cec17e5f91cf0a6841761e13a6df59a365a0 --- .../impl/DeclarativeHintsEditorInitializer.kt | 134 +++++++++++++++ .../daemon/impl/DeclarativeHintsState.kt | 43 +++++ .../daemon/impl/HighlightingMarkupStore.kt | 3 +- .../daemon/impl/grave/CodeVisionState.kt | 4 +- .../codeInsight/hints/ParameterHintsState.kt | 4 +- .../impl/DeclarativeInlayHintsPass.kt | 140 ++++++++-------- .../impl/DeclarativeInlayRenderer.kt | 7 +- .../hints/declarative/impl/InlayData.kt | 157 ++++++++++++++++++ .../declarative/impl/InlayPresentationList.kt | 15 ++ .../declarative/impl/InlayTreeSinkImpl.kt | 17 -- .../impl/PresentationEntryBuilder.kt | 14 +- .../impl/PresentationTreeExternalizer.kt | 110 ++++++++++++ .../declarative/impl/ZombieSmartPointer.kt | 21 +++ .../hints/declarative/impl/util/TinyTree.kt | 64 ++++++- .../impl/text/foldingGrave/FoldingState.kt | 4 +- .../hints/declarative/TinyTreeTest.kt | 16 ++ .../fileEditor/impl/text/TextEditorCache.kt | 6 +- .../impl/text/VersionedExternalizer.kt | 10 ++ .../src/META-INF/PlatformLangComponents.xml | 1 + 19 files changed, 665 insertions(+), 105 deletions(-) create mode 100644 platform/lang-impl/src/com/intellij/codeInsight/daemon/impl/DeclarativeHintsEditorInitializer.kt create mode 100644 platform/lang-impl/src/com/intellij/codeInsight/daemon/impl/DeclarativeHintsState.kt create mode 100644 platform/lang-impl/src/com/intellij/codeInsight/hints/declarative/impl/InlayData.kt create mode 100644 platform/lang-impl/src/com/intellij/codeInsight/hints/declarative/impl/PresentationTreeExternalizer.kt create mode 100644 platform/lang-impl/src/com/intellij/codeInsight/hints/declarative/impl/ZombieSmartPointer.kt create mode 100644 platform/platform-impl/src/com/intellij/openapi/fileEditor/impl/text/VersionedExternalizer.kt diff --git a/platform/lang-impl/src/com/intellij/codeInsight/daemon/impl/DeclarativeHintsEditorInitializer.kt b/platform/lang-impl/src/com/intellij/codeInsight/daemon/impl/DeclarativeHintsEditorInitializer.kt new file mode 100644 index 000000000000..83a0649458ad --- /dev/null +++ b/platform/lang-impl/src/com/intellij/codeInsight/daemon/impl/DeclarativeHintsEditorInitializer.kt @@ -0,0 +1,134 @@ +// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.codeInsight.daemon.impl + +import com.intellij.codeInsight.hints.declarative.InlayActionPayload +import com.intellij.codeInsight.hints.declarative.PsiPointerInlayActionPayload +import com.intellij.codeInsight.hints.declarative.impl.* +import com.intellij.codeInsight.hints.declarative.impl.util.TinyTree +import com.intellij.openapi.Disposable +import com.intellij.openapi.application.EDT +import com.intellij.openapi.application.readActionBlocking +import com.intellij.openapi.components.Service +import com.intellij.openapi.components.Service.Level +import com.intellij.openapi.components.serviceAsync +import com.intellij.openapi.editor.Document +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.editor.EditorFactory +import com.intellij.openapi.editor.EditorKind +import com.intellij.openapi.editor.event.EditorFactoryEvent +import com.intellij.openapi.editor.event.EditorFactoryListener +import com.intellij.openapi.editor.ex.EditorEx +import com.intellij.openapi.fileEditor.impl.text.TextEditorCache +import com.intellij.openapi.fileEditor.impl.text.TextEditorInitializer +import com.intellij.openapi.project.Project +import com.intellij.openapi.util.registry.Registry +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.openapi.vfs.VirtualFileWithId +import com.intellij.psi.PsiManager +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +internal class DeclarativeHintsEditorInitializer : TextEditorInitializer { + override suspend fun initializeEditor( + project: Project, + file: VirtualFile, + document: Document, + editorSupplier: suspend () -> EditorEx, + highlighterReady: suspend () -> Unit, + ) { + val grave = project.serviceAsync() + val inlayDataList = grave.raise(file, document) ?: return + val psiManager = project.serviceAsync() + val psiFile = readActionBlocking { + psiManager.findFile(file) + } ?: return + val editor = editorSupplier() + withContext(Dispatchers.EDT) { + DeclarativeInlayHintsPass.applyInlayData(editor, psiFile, inlayDataList) + } + } +} + +@Service(Level.PROJECT) +internal class DeclarativeHintsGrave(private val project: Project, private val scope: CoroutineScope) + : TextEditorCache(project, scope), + Disposable { + + override fun namePrefix(): String = "persistent-declarative-hints" + override fun valueExternalizer(): DeclarativeHintsState.Externalizer = DeclarativeHintsState.Externalizer() + override fun useHeapCache(): Boolean = true + + init { + subscribeEditorClosed() + } + + fun raise(file: VirtualFile, document: Document): List? { + if (!isEnabled() || file !is VirtualFileWithId) { + return null + } + val state: DeclarativeHintsState? = cache[file.id] + if (state == null || state.contentHash != document.contentHash()) { + return null + } + for (inlayData in state.inlayDataList) { + initZombiePointers(file, inlayData.tree) + } + return state.inlayDataList + } + + private fun bury(editor: Editor) { + val file = editor.virtualFile + if (!isEnabled() || editor.editorKind != EditorKind.MAIN_EDITOR || file !is VirtualFileWithId) { + return + } + val declarativeHints = editor.getInlayModel().getInlineElementsInRange( + 0, + editor.getDocument().textLength, + DeclarativeInlayRenderer::class.java + ) + if (declarativeHints.isEmpty()) { + return + } + val contentHash = editor.document.contentHash() + val inlayDataList = declarativeHints.map { inlay -> inlay.renderer.toInlayData() }.toList() + scope.launch(Dispatchers.IO) { + cache[file.id] = DeclarativeHintsState(contentHash, inlayDataList) + } + } + + override fun dispose() { + } + + private fun subscribeEditorClosed() { + EditorFactory.getInstance().addEditorFactoryListener( + object : EditorFactoryListener { + override fun editorReleased(event: EditorFactoryEvent) { + bury(event.editor) + } + }, + this + ) + } + + private fun initZombiePointers(file: VirtualFile, tree: TinyTree, index: Byte = 0) { + val dataPayload = tree.getDataPayload(index) + if (dataPayload is ActionWithContent) { + val payload: InlayActionPayload = dataPayload.actionData.payload + if (payload is PsiPointerInlayActionPayload) { + val pointer = payload.pointer + if (pointer is ZombieSmartPointer) { + pointer.projectSupp = { project } + pointer.fileSupp = { file } + } + } + } + tree.processChildren(index) { child -> + initZombiePointers(file, tree, child) + true + } + } + + private fun isEnabled() = Registry.`is`("cache.inlay.hints.on.disk") +} diff --git a/platform/lang-impl/src/com/intellij/codeInsight/daemon/impl/DeclarativeHintsState.kt b/platform/lang-impl/src/com/intellij/codeInsight/daemon/impl/DeclarativeHintsState.kt new file mode 100644 index 000000000000..e8143fdd8b95 --- /dev/null +++ b/platform/lang-impl/src/com/intellij/codeInsight/daemon/impl/DeclarativeHintsState.kt @@ -0,0 +1,43 @@ +// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.codeInsight.daemon.impl + +import com.intellij.codeInsight.hints.declarative.impl.InlayData +import com.intellij.openapi.fileEditor.impl.text.VersionedExternalizer +import com.intellij.util.io.DataInputOutputUtil.readINT +import com.intellij.util.io.DataInputOutputUtil.writeINT +import java.io.DataInput +import java.io.DataOutput + +internal class DeclarativeHintsState(val contentHash: Int, val inlayDataList: List) { + + class Externalizer : VersionedExternalizer { + + private val inlayDataExternalizer: InlayData.Externalizer = InlayData.Externalizer() + + companion object { + // increment on format changed + private const val SERDE_VERSION = 0 + } + + override fun serdeVersion(): Int = SERDE_VERSION + inlayDataExternalizer.serdeVersion() + + override fun save(output: DataOutput, state: DeclarativeHintsState) { + writeINT(output, state.contentHash) + writeINT(output, state.inlayDataList.size) + for (inlayData in state.inlayDataList) { + inlayDataExternalizer.save(output, inlayData) + } + } + + override fun read(input: DataInput): DeclarativeHintsState { + val contentHash = readINT(input) + val inlayCount = readINT(input) + val inlayDataList = buildList(inlayCount) { + repeat(inlayCount) { + add(inlayDataExternalizer.read(input)) + } + } + return DeclarativeHintsState(contentHash, inlayDataList) + } + } +} diff --git a/platform/lang-impl/src/com/intellij/codeInsight/daemon/impl/HighlightingMarkupStore.kt b/platform/lang-impl/src/com/intellij/codeInsight/daemon/impl/HighlightingMarkupStore.kt index 6c6593eb0f04..f6fc8ead6e4b 100644 --- a/platform/lang-impl/src/com/intellij/codeInsight/daemon/impl/HighlightingMarkupStore.kt +++ b/platform/lang-impl/src/com/intellij/codeInsight/daemon/impl/HighlightingMarkupStore.kt @@ -3,6 +3,7 @@ package com.intellij.codeInsight.daemon.impl import com.intellij.codeInsight.daemon.impl.HighlightingMarkupGrave.FileMarkupInfo import com.intellij.openapi.fileEditor.impl.text.TextEditorCache +import com.intellij.openapi.fileEditor.impl.text.VersionedExternalizer import com.intellij.openapi.project.Project import com.intellij.openapi.vfs.VirtualFileWithId import kotlinx.coroutines.CoroutineScope @@ -34,7 +35,7 @@ internal class HighlightingMarkupStore(project: Project, private val scope: Coro } } - object FileMarkupInfoExternalizer : ValueExternalizer { + object FileMarkupInfoExternalizer : VersionedExternalizer { override fun serdeVersion() = 2 override fun save(output: DataOutput, value: FileMarkupInfo) = value.bury(output) override fun read(input: DataInput) = FileMarkupInfo.exhume(input) diff --git a/platform/lang-impl/src/com/intellij/codeInsight/daemon/impl/grave/CodeVisionState.kt b/platform/lang-impl/src/com/intellij/codeInsight/daemon/impl/grave/CodeVisionState.kt index 491d7a550b2c..983e8ce0b55d 100644 --- a/platform/lang-impl/src/com/intellij/codeInsight/daemon/impl/grave/CodeVisionState.kt +++ b/platform/lang-impl/src/com/intellij/codeInsight/daemon/impl/grave/CodeVisionState.kt @@ -6,7 +6,7 @@ import com.intellij.codeInsight.codeVision.ui.model.CounterCodeVisionEntry import com.intellij.codeInsight.codeVision.ui.model.ZombieCodeVisionEntry import com.intellij.codeInsight.daemon.impl.readGutterIcon import com.intellij.codeInsight.daemon.impl.writeGutterIcon -import com.intellij.openapi.fileEditor.impl.text.TextEditorCache +import com.intellij.openapi.fileEditor.impl.text.VersionedExternalizer import com.intellij.openapi.util.TextRange import com.intellij.util.io.DataInputOutputUtil.readINT import com.intellij.util.io.DataInputOutputUtil.writeINT @@ -21,7 +21,7 @@ internal data class CodeVisionState(val contentHash: Int, val entries: List { + object Externalizer : VersionedExternalizer { override fun serdeVersion() = 0 override fun save(output: DataOutput, value: CodeVisionState) { diff --git a/platform/lang-impl/src/com/intellij/codeInsight/hints/ParameterHintsState.kt b/platform/lang-impl/src/com/intellij/codeInsight/hints/ParameterHintsState.kt index b7695408fbcb..f3c6ed3ca2da 100644 --- a/platform/lang-impl/src/com/intellij/codeInsight/hints/ParameterHintsState.kt +++ b/platform/lang-impl/src/com/intellij/codeInsight/hints/ParameterHintsState.kt @@ -2,7 +2,7 @@ package com.intellij.codeInsight.hints import com.intellij.codeInsight.hints.ParameterHintsPass.HintData -import com.intellij.openapi.fileEditor.impl.text.TextEditorCache +import com.intellij.openapi.fileEditor.impl.text.VersionedExternalizer import com.intellij.util.io.DataInputOutputUtil.readINT import com.intellij.util.io.DataInputOutputUtil.writeINT import com.intellij.util.io.IOUtil.readUTF @@ -12,7 +12,7 @@ import java.io.DataOutput internal data class ParameterHintsState(val contentHash: Int, val hints: List>) { - object Externalizer : TextEditorCache.ValueExternalizer { + object Externalizer : VersionedExternalizer { override fun serdeVersion() = 0 override fun save(output: DataOutput, value: ParameterHintsState) { diff --git a/platform/lang-impl/src/com/intellij/codeInsight/hints/declarative/impl/DeclarativeInlayHintsPass.kt b/platform/lang-impl/src/com/intellij/codeInsight/hints/declarative/impl/DeclarativeInlayHintsPass.kt index bfb23ee05999..2a8d36d5cb91 100644 --- a/platform/lang-impl/src/com/intellij/codeInsight/hints/declarative/impl/DeclarativeInlayHintsPass.kt +++ b/platform/lang-impl/src/com/intellij/codeInsight/hints/declarative/impl/DeclarativeInlayHintsPass.kt @@ -9,8 +9,10 @@ import com.intellij.openapi.editor.Inlay import com.intellij.openapi.progress.ProgressIndicator import com.intellij.openapi.util.Disposer import com.intellij.psi.PsiElement +import com.intellij.psi.PsiFile import com.intellij.psi.SyntaxTraverser import com.intellij.util.SmartList +import com.intellij.util.concurrency.annotations.RequiresEdt import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap import java.util.function.IntFunction @@ -53,86 +55,92 @@ class DeclarativeInlayHintsPass( ) override fun doApplyInformationToEditor() { - val inlayDatas = sinks.flatMap { it.finish() } - val inlayModel = editor.inlayModel - val document = editor.document - val existingInlineElements = inlayModel.getInlineElementsInRange(0, document.textLength, DeclarativeInlayRenderer::class.java) - val existingEolElements = inlayModel.getAfterLineEndElementsInRange(0, document.textLength, DeclarativeInlayRenderer::class.java) - val offsetToExistingInlineElements = Int2ObjectOpenHashMap>>() // either inlay or list of inlays - val offsetToExistingEolElements = Int2ObjectOpenHashMap>>() // either inlay or list of inlays - for (inlineElement in existingInlineElements) { - val inlaysAtOffset = offsetToExistingInlineElements.computeIfAbsent(inlineElement.offset, IntFunction { SmartList() }) - inlaysAtOffset.add(inlineElement) - } - for (eolElement in existingEolElements) { - val inlaysAtOffset = offsetToExistingEolElements.computeIfAbsent(eolElement.offset, IntFunction { SmartList() }) - inlaysAtOffset.add(eolElement) - } - val storage = InlayHintsUtils.getTextMetricStorage(editor) - for (inlayData in inlayDatas) { - when (val position = inlayData.position) { - is EndOfLinePosition -> { - val lineEndOffset = editor.document.getLineEndOffset(position.line) - val updated = tryUpdateAndDeleteFromListInlay(offsetToExistingEolElements, inlayData, lineEndOffset) - if (!updated) { - val presentationList = InlayPresentationList(inlayData.tree, inlayData.hasBackground, inlayData.disabled, - createPayloads(inlayData), inlayData.providerClass, inlayData.tooltip) - val renderer = DeclarativeInlayRenderer(presentationList, storage, inlayData.providerId) - val inlay = inlayModel.addAfterLineEndElement(lineEndOffset, true, renderer) - if (inlay != null) { - renderer.setInlay(inlay) + applyInlayData(editor, myFile, inlayDatas = sinks.flatMap { it.finish() }) + } + + companion object { + @RequiresEdt + internal fun applyInlayData(editor: Editor, file: PsiFile, inlayDatas: List) { + val inlayModel = editor.inlayModel + val document = editor.document + val existingInlineElements = inlayModel.getInlineElementsInRange(0, document.textLength, DeclarativeInlayRenderer::class.java) + val existingEolElements = inlayModel.getAfterLineEndElementsInRange(0, document.textLength, DeclarativeInlayRenderer::class.java) + val offsetToExistingInlineElements = Int2ObjectOpenHashMap>>() // either inlay or list of inlays + val offsetToExistingEolElements = Int2ObjectOpenHashMap>>() // either inlay or list of inlays + for (inlineElement in existingInlineElements) { + val inlaysAtOffset = offsetToExistingInlineElements.computeIfAbsent(inlineElement.offset, IntFunction { SmartList() }) + inlaysAtOffset.add(inlineElement) + } + for (eolElement in existingEolElements) { + val inlaysAtOffset = offsetToExistingEolElements.computeIfAbsent(eolElement.offset, IntFunction { SmartList() }) + inlaysAtOffset.add(eolElement) + } + val storage = InlayHintsUtils.getTextMetricStorage(editor) + for (inlayData in inlayDatas) { + when (val position = inlayData.position) { + is EndOfLinePosition -> { + val lineEndOffset = editor.document.getLineEndOffset(position.line) + val updated = tryUpdateAndDeleteFromListInlay(offsetToExistingEolElements, inlayData, lineEndOffset) + if (!updated) { + val presentationList = InlayPresentationList(inlayData.tree, inlayData.hasBackground, inlayData.disabled, + createPayloads(inlayData), inlayData.providerClass, inlayData.tooltip) + val renderer = DeclarativeInlayRenderer(presentationList, storage, inlayData.providerId, position) + val inlay = inlayModel.addAfterLineEndElement(lineEndOffset, true, renderer) + if (inlay != null) { + renderer.setInlay(inlay) + } } } - } - is InlineInlayPosition -> { - val updated = tryUpdateAndDeleteFromListInlay(offsetToExistingInlineElements, inlayData, position.offset) - if (!updated) { - val presentationList = InlayPresentationList(inlayData.tree, inlayData.hasBackground, inlayData.disabled, - createPayloads(inlayData), inlayData.providerClass, inlayData.tooltip) - val renderer = DeclarativeInlayRenderer(presentationList, storage, inlayData.providerId) - val inlay = inlayModel.addInlineElement(position.offset, position.relatedToPrevious, position.priority, renderer) - if (inlay != null) { - renderer.setInlay(inlay) + is InlineInlayPosition -> { + val updated = tryUpdateAndDeleteFromListInlay(offsetToExistingInlineElements, inlayData, position.offset) + if (!updated) { + val presentationList = InlayPresentationList(inlayData.tree, inlayData.hasBackground, inlayData.disabled, + createPayloads(inlayData), inlayData.providerClass, inlayData.tooltip) + val renderer = DeclarativeInlayRenderer(presentationList, storage, inlayData.providerId, position) + val inlay = inlayModel.addInlineElement(position.offset, position.relatedToPrevious, position.priority, renderer) + if (inlay != null) { + renderer.setInlay(inlay) + } } } } } + + deleteNotPreservedInlays(offsetToExistingInlineElements) + deleteNotPreservedInlays(offsetToExistingEolElements) + + DeclarativeInlayHintsPassFactory.updateModificationStamp(editor, file) } - deleteNotPreservedInlays(offsetToExistingInlineElements) - deleteNotPreservedInlays(offsetToExistingEolElements) + private fun createPayloads(inlayData: InlayData) = + inlayData.payloads?.associate { it.payloadName to it.payload } - DeclarativeInlayHintsPassFactory.updateModificationStamp(editor, myFile) - } - - private fun createPayloads(inlayData: InlayData) = - inlayData.payloads?.associate { it.payloadName to it.payload } - - private fun deleteNotPreservedInlays(offsetToExistingInlays: Int2ObjectOpenHashMap>>) { - for (inlays in offsetToExistingInlays.values) { - for (inlay in inlays) { - Disposer.dispose(inlay) + private fun deleteNotPreservedInlays(offsetToExistingInlays: Int2ObjectOpenHashMap>>) { + for (inlays in offsetToExistingInlays.values) { + for (inlay in inlays) { + Disposer.dispose(inlay) + } } } - } - private fun tryUpdateAndDeleteFromListInlay(offsetToExistingInlays: Int2ObjectOpenHashMap>>, - inlayData: InlayData, - offset: Int): Boolean { - val inlays = offsetToExistingInlays.get(offset) - if (inlays == null) return false - val iterator = inlays.iterator() - while (iterator.hasNext()) { - val existingInlay = iterator.next() - val renderer = existingInlay.renderer - if (renderer.providerId == inlayData.providerId) { - renderer.updateState(inlayData.tree, inlayData.disabled, inlayData.hasBackground) - existingInlay.update() - iterator.remove() - return true + private fun tryUpdateAndDeleteFromListInlay(offsetToExistingInlays: Int2ObjectOpenHashMap>>, + inlayData: InlayData, + offset: Int): Boolean { + val inlays = offsetToExistingInlays.get(offset) + if (inlays == null) return false + val iterator = inlays.iterator() + while (iterator.hasNext()) { + val existingInlay = iterator.next() + val renderer = existingInlay.renderer + if (renderer.providerId == inlayData.providerId) { + renderer.updateState(inlayData.tree, inlayData.disabled, inlayData.hasBackground) + existingInlay.update() + iterator.remove() + return true + } } + return false } - return false } private fun createCollector(provider: InlayHintsProvider): InlayHintsCollector? { diff --git a/platform/lang-impl/src/com/intellij/codeInsight/hints/declarative/impl/DeclarativeInlayRenderer.kt b/platform/lang-impl/src/com/intellij/codeInsight/hints/declarative/impl/DeclarativeInlayRenderer.kt index 3d43cb14c019..29e7a292663e 100644 --- a/platform/lang-impl/src/com/intellij/codeInsight/hints/declarative/impl/DeclarativeInlayRenderer.kt +++ b/platform/lang-impl/src/com/intellij/codeInsight/hints/declarative/impl/DeclarativeInlayRenderer.kt @@ -3,6 +3,7 @@ package com.intellij.codeInsight.hints.declarative.impl import com.intellij.codeInsight.hints.declarative.InlayHintsProvider import com.intellij.codeInsight.hints.declarative.InlayHintsProviderFactory +import com.intellij.codeInsight.hints.declarative.InlayPosition import com.intellij.codeInsight.hints.declarative.impl.util.TinyTree import com.intellij.codeInsight.hints.presentation.InlayTextMetricsStorage import com.intellij.openapi.actionSystem.ActionGroup @@ -28,6 +29,7 @@ class DeclarativeInlayRenderer( val presentationList: InlayPresentationList, private val fontMetricsStorage: InlayTextMetricsStorage, val providerId: String, + private val position: InlayPosition, ) : EditorCustomElementRenderer { private var inlay: Inlay? = null @@ -89,5 +91,8 @@ class DeclarativeInlayRenderer( override fun getContextMenuGroupId(inlay: Inlay<*>): String { return "DummyActionGroup" } -} + internal fun toInlayData(): InlayData { + return presentationList.toInlayData(position, providerId) + } +} diff --git a/platform/lang-impl/src/com/intellij/codeInsight/hints/declarative/impl/InlayData.kt b/platform/lang-impl/src/com/intellij/codeInsight/hints/declarative/impl/InlayData.kt new file mode 100644 index 000000000000..454f2440e25c --- /dev/null +++ b/platform/lang-impl/src/com/intellij/codeInsight/hints/declarative/impl/InlayData.kt @@ -0,0 +1,157 @@ +// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.codeInsight.hints.declarative.impl + +import com.intellij.codeInsight.hints.declarative.EndOfLinePosition +import com.intellij.codeInsight.hints.declarative.InlayPayload +import com.intellij.codeInsight.hints.declarative.InlayPosition +import com.intellij.codeInsight.hints.declarative.InlineInlayPosition +import com.intellij.codeInsight.hints.declarative.impl.util.TinyTree +import com.intellij.openapi.fileEditor.impl.text.VersionedExternalizer +import com.intellij.openapi.util.NlsContexts +import com.intellij.util.io.DataInputOutputUtil.readINT +import com.intellij.util.io.DataInputOutputUtil.writeINT +import com.intellij.util.io.IOUtil.readUTF +import com.intellij.util.io.IOUtil.writeUTF +import java.io.DataInput +import java.io.DataOutput + +data class InlayData( + val position: InlayPosition, + @NlsContexts.HintText val tooltip: String?, + val hasBackground: Boolean, + val tree: TinyTree, + val providerId: String, + val disabled: Boolean, + val payloads: List?, + /** + * Just for debugging purposes + */ + val providerClass: Class<*>, +) { + + class Externalizer : VersionedExternalizer { + private val treeExternalizer: PresentationTreeExternalizer = PresentationTreeExternalizer() + + companion object { + // increment on format changed + private const val SERDE_VERSION = 0 + } + + override fun serdeVersion(): Int = SERDE_VERSION + treeExternalizer.serdeVersion() + + override fun save(output: DataOutput, inlayData: InlayData) { + writePosition(output, inlayData.position) + writeTooltip(output, inlayData.tooltip) + output.writeBoolean(inlayData.hasBackground) + treeExternalizer.save(output, inlayData.tree) + writeUTF(output, inlayData.providerId) + output.writeBoolean(inlayData.disabled) + writePayloads(output, inlayData.payloads) + writeProviderClass(output, inlayData.providerClass) + } + + override fun read(input: DataInput): InlayData { + val position: InlayPosition = readPosition(input) + val tooltip: String? = readTooltip(input) + val hasBackground: Boolean = input.readBoolean() + val tree: TinyTree = treeExternalizer.read(input) + val providerId: String = readUTF(input) + val disabled: Boolean = input.readBoolean() + val payloads: List? = readPayloads(input) + val providerClass: Class<*> = readProviderClass(input) + return InlayData(position, tooltip, hasBackground, tree, providerId, disabled, payloads, providerClass) + } + + private fun writePosition(output: DataOutput, position: InlayPosition) { + when (position) { + is InlineInlayPosition -> { + writeINT(output, 0) + writeINT(output, position.offset) + output.writeBoolean(position.relatedToPrevious) + writeINT(output, position.priority) + } + is EndOfLinePosition -> { + writeINT(output, 1) + writeINT(output, position.line) + } + } + } + + private fun readPosition(input: DataInput): InlayPosition { + val type = readINT(input) + if (type == 0) { + val offset = readINT(input) + val related = input.readBoolean() + val priority = readINT(input) + return InlineInlayPosition(offset, related, priority) + } else if (type == 1) { + val line = readINT(input) + return EndOfLinePosition(line) + } + throw IllegalStateException("unknown inlay position type: $type") + } + + private fun writeTooltip(output: DataOutput, tooltip: String?) { + if (tooltip == null) { + output.writeBoolean(false) + } else { + output.writeBoolean(true) + writeUTF(output, tooltip) + } + } + + private fun readTooltip(input: DataInput): String? { + return if (input.readBoolean()) { + readUTF(input) + } else { + null + } + } + + private fun writePayloads(output: DataOutput, payloads: List?) { + if (payloads == null) { + output.writeBoolean(false) + } else { + output.writeBoolean(true) + writeINT(output, payloads.size) + for (p in payloads) { + writeInlayPayload(output, p) + } + } + } + + private fun readPayloads(input: DataInput): List? { + if (input.readBoolean()) { + val payloadCount = readINT(input) + val payloads = ArrayList(payloadCount) + repeat(payloadCount) { + val inlayPayload = readInlayPayload(input) + payloads.add(inlayPayload) + } + return payloads + } else { + return null + } + } + + private fun writeInlayPayload(output: DataOutput, payload: InlayPayload) { + writeUTF(output, payload.payloadName) + treeExternalizer.writeInlayActionPayload(output, payload.payload) + } + + private fun readInlayPayload(input: DataInput): InlayPayload { + val payloadName = readUTF(input) + val inlayActionPayload = treeExternalizer.readInlayActionPayload(input) + return InlayPayload(payloadName, inlayActionPayload) + } + + private fun writeProviderClass(output: DataOutput, providerClass: Class<*>) { + writeUTF(output, providerClass.name) + } + + private fun readProviderClass(input: DataInput): Class<*> { + val className = readUTF(input) + return Class.forName(className) + } + } +} diff --git a/platform/lang-impl/src/com/intellij/codeInsight/hints/declarative/impl/InlayPresentationList.kt b/platform/lang-impl/src/com/intellij/codeInsight/hints/declarative/impl/InlayPresentationList.kt index ef7925bec5e8..7ffdcea05ea2 100644 --- a/platform/lang-impl/src/com/intellij/codeInsight/hints/declarative/impl/InlayPresentationList.kt +++ b/platform/lang-impl/src/com/intellij/codeInsight/hints/declarative/impl/InlayPresentationList.kt @@ -3,6 +3,8 @@ package com.intellij.codeInsight.hints.declarative.impl import com.intellij.codeInsight.hints.InlayHintsUtils import com.intellij.codeInsight.hints.declarative.InlayActionPayload +import com.intellij.codeInsight.hints.declarative.InlayPayload +import com.intellij.codeInsight.hints.declarative.InlayPosition import com.intellij.codeInsight.hints.declarative.impl.util.TinyTree import com.intellij.codeInsight.hints.presentation.InlayTextMetricsStorage import com.intellij.codeInsight.hints.presentation.PresentationFactory @@ -207,4 +209,17 @@ class InlayPresentationList( val entry = findEntryByPoint(fontMetricsStorage, pointInsideInlay) ?: return null return entry.clickArea } + + internal fun toInlayData(position: InlayPosition, providerId: String): InlayData { + return InlayData( + position, + tooltip, + hasBackground, + state, + providerId, + isDisabled, + payloads?.map { (name, action) -> InlayPayload(name, action) }, + providerClass, + ) + } } \ No newline at end of file diff --git a/platform/lang-impl/src/com/intellij/codeInsight/hints/declarative/impl/InlayTreeSinkImpl.kt b/platform/lang-impl/src/com/intellij/codeInsight/hints/declarative/impl/InlayTreeSinkImpl.kt index 6ebb342dfe9f..12353ac6890d 100644 --- a/platform/lang-impl/src/com/intellij/codeInsight/hints/declarative/impl/InlayTreeSinkImpl.kt +++ b/platform/lang-impl/src/com/intellij/codeInsight/hints/declarative/impl/InlayTreeSinkImpl.kt @@ -5,7 +5,6 @@ import com.intellij.codeInsight.hints.declarative.InlayPayload import com.intellij.codeInsight.hints.declarative.InlayPosition import com.intellij.codeInsight.hints.declarative.InlayTreeSink import com.intellij.codeInsight.hints.declarative.PresentationTreeBuilder -import com.intellij.codeInsight.hints.declarative.impl.util.TinyTree import com.intellij.diagnostic.PluginException import com.intellij.openapi.util.NlsContexts @@ -73,19 +72,3 @@ class InlayTreeSinkImpl( return inlayDataToPresentation } } - -data class InlayData( - val position: InlayPosition, - @NlsContexts.HintText val tooltip: String?, - val hasBackground: Boolean, - val tree: TinyTree, - val providerId: String, - val disabled: Boolean, - val payloads: List?, - /** - * Just for debugging purposes - */ - val providerClass: Class<*> -) { - -} \ No newline at end of file diff --git a/platform/lang-impl/src/com/intellij/codeInsight/hints/declarative/impl/PresentationEntryBuilder.kt b/platform/lang-impl/src/com/intellij/codeInsight/hints/declarative/impl/PresentationEntryBuilder.kt index 7d8e343de90f..a0a939215062 100644 --- a/platform/lang-impl/src/com/intellij/codeInsight/hints/declarative/impl/PresentationEntryBuilder.kt +++ b/platform/lang-impl/src/com/intellij/codeInsight/hints/declarative/impl/PresentationEntryBuilder.kt @@ -2,6 +2,7 @@ package com.intellij.codeInsight.hints.declarative.impl import com.intellij.codeInsight.hints.declarative.InlayActionData +import com.intellij.codeInsight.hints.declarative.PsiPointerInlayActionPayload import com.intellij.codeInsight.hints.declarative.impl.util.TinyTree import com.intellij.diagnostic.PluginException @@ -53,10 +54,10 @@ class PresentationEntryBuilder(val state: TinyTree, private val providerCl area?.entries?.add(entry) } is ActionWithContent -> { - val area = this.currentClickArea ?: InlayMouseArea(dataPayload.actionData) + val area = this.currentClickArea ?: mouseAreaIfNotZombie(dataPayload.actionData) val entry = TextInlayPresentationEntry(dataPayload.content as String, clickArea = area, parentIndexToSwitch = parentIndexToSwitch) addEntry(entry) - area.entries.add(entry) + area?.entries?.add(entry) } else -> throw IllegalStateException("Illegal payload for text tag: $dataPayload") } @@ -85,6 +86,15 @@ class PresentationEntryBuilder(val state: TinyTree, private val providerCl } } + private fun mouseAreaIfNotZombie(actionData: InlayActionData): InlayMouseArea? { + val inlayActionPayload = actionData.payload + if (inlayActionPayload is PsiPointerInlayActionPayload && inlayActionPayload.pointer is ZombieSmartPointer) { + // zombie pointer is not hoverable/clickable + return null + } + return InlayMouseArea(actionData) + } + private fun selectFromList(index: Byte, collapsed: Boolean) { val savedIndexOfClosestParentList = indexOfClosestParentList indexOfClosestParentList = index diff --git a/platform/lang-impl/src/com/intellij/codeInsight/hints/declarative/impl/PresentationTreeExternalizer.kt b/platform/lang-impl/src/com/intellij/codeInsight/hints/declarative/impl/PresentationTreeExternalizer.kt new file mode 100644 index 000000000000..dead084162ea --- /dev/null +++ b/platform/lang-impl/src/com/intellij/codeInsight/hints/declarative/impl/PresentationTreeExternalizer.kt @@ -0,0 +1,110 @@ +// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.codeInsight.hints.declarative.impl + +import com.intellij.codeInsight.hints.declarative.InlayActionData +import com.intellij.codeInsight.hints.declarative.InlayActionPayload +import com.intellij.codeInsight.hints.declarative.PsiPointerInlayActionPayload +import com.intellij.codeInsight.hints.declarative.StringInlayActionPayload +import com.intellij.codeInsight.hints.declarative.impl.util.TinyTree +import com.intellij.openapi.util.registry.Registry +import com.intellij.util.io.DataInputOutputUtil.readINT +import com.intellij.util.io.DataInputOutputUtil.writeINT +import com.intellij.util.io.IOUtil.readUTF +import com.intellij.util.io.IOUtil.writeUTF +import java.io.DataInput +import java.io.DataOutput + +internal class PresentationTreeExternalizer : TinyTree.Externalizer() { + + companion object { + // increment on format changed + private const val SERDE_VERSION = 0 + } + + override fun serdeVersion(): Int = SERDE_VERSION + super.serdeVersion() + + override fun writeDataPayload(output: DataOutput, payload: Any?) { + when (payload) { + is String? -> { + writeINT(output, 0) + writeNullableString(output, payload) + } + is ActionWithContent -> { + writeINT(output, 1) + writeUTF(output, payload.content as String) + writeInlayActionData(output, payload.actionData) + } + else -> throw IllegalArgumentException("unknown data payload type: $payload") + } + } + + override fun readDataPayload(input: DataInput): Any? { + val type = readINT(input) + if (type == 0) { + val string = readNullableString(input) ?: return null + return decorateIfDebug(string) + } else if (type == 1) { + val content = decorateIfDebug(readUTF(input)) + return ActionWithContent(readInlayActionData(input), content) + } + throw IllegalStateException("unknown data payload type: $type") + } + + private fun writeNullableString(output: DataOutput, value: String?) { + if (value == null) { + output.writeBoolean(false) + } else { + output.writeBoolean(true) + writeUTF(output, value) + } + } + + private fun readNullableString(input: DataInput): String? { + return if (input.readBoolean()) { + readUTF(input) + } else { + null + } + } + + private fun writeInlayActionData(output: DataOutput, inlayActionData: InlayActionData) { + writeUTF(output, inlayActionData.handlerId) + writeInlayActionPayload(output, inlayActionData.payload) + } + + private fun readInlayActionData(input: DataInput): InlayActionData { + val handlerId = readUTF(input) + val payload = readInlayActionPayload(input) + return InlayActionData(payload, handlerId) + } + + fun writeInlayActionPayload(output: DataOutput, actionPayload: InlayActionPayload) { + when(actionPayload) { + is StringInlayActionPayload -> { + writeINT(output, 0) + writeUTF(output, actionPayload.text) + } + is PsiPointerInlayActionPayload -> { + writeINT(output, 1) + } + } + } + + fun readInlayActionPayload(input: DataInput): InlayActionPayload { + val type = readINT(input) + if (type == 0) { + return StringInlayActionPayload(readUTF(input)) + } else if (type == 1) { + return PsiPointerInlayActionPayload(ZombieSmartPointer()) + } + throw IllegalStateException("unknown inlay action payload type: $type") + } + + private fun decorateIfDebug(content: String): String { + return if (Registry.`is`("cache.markup.debug")) { + "$content?" + } else { + content + } + } +} diff --git a/platform/lang-impl/src/com/intellij/codeInsight/hints/declarative/impl/ZombieSmartPointer.kt b/platform/lang-impl/src/com/intellij/codeInsight/hints/declarative/impl/ZombieSmartPointer.kt new file mode 100644 index 000000000000..81765ed3b528 --- /dev/null +++ b/platform/lang-impl/src/com/intellij/codeInsight/hints/declarative/impl/ZombieSmartPointer.kt @@ -0,0 +1,21 @@ +// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.codeInsight.hints.declarative.impl + +import com.intellij.openapi.project.Project +import com.intellij.openapi.util.Segment +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiFile +import com.intellij.psi.SmartPsiElementPointer + +internal class ZombieSmartPointer : SmartPsiElementPointer { + lateinit var projectSupp: () -> Project + lateinit var fileSupp: () -> VirtualFile + + override fun getElement(): PsiElement? = null + override fun getContainingFile(): PsiFile? = null + override fun getProject(): Project = projectSupp.invoke() + override fun getVirtualFile(): VirtualFile = fileSupp.invoke() + override fun getRange(): Segment? = null + override fun getPsiRange(): Segment? = null +} diff --git a/platform/lang-impl/src/com/intellij/codeInsight/hints/declarative/impl/util/TinyTree.kt b/platform/lang-impl/src/com/intellij/codeInsight/hints/declarative/impl/util/TinyTree.kt index 30bdead1d270..1cc7ec09e3f3 100644 --- a/platform/lang-impl/src/com/intellij/codeInsight/hints/declarative/impl/util/TinyTree.kt +++ b/platform/lang-impl/src/com/intellij/codeInsight/hints/declarative/impl/util/TinyTree.kt @@ -1,22 +1,27 @@ // Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package com.intellij.codeInsight.hints.declarative.impl.util +import com.intellij.openapi.fileEditor.impl.text.VersionedExternalizer +import com.intellij.util.io.DataInputOutputUtil.readINT +import com.intellij.util.io.DataInputOutputUtil.writeINT import it.unimi.dsi.fastutil.bytes.ByteArrayList +import java.io.DataInput +import java.io.DataOutput /** * Stores up to 127 elements with a single byte payload and reference data */ -class TinyTree(rootPayload: Byte, rootData: T) { +class TinyTree private constructor( + private val firstChild: ByteArrayList, + private val nextChild: ByteArrayList, + private val payload: ByteArrayList, + private val data: ArrayList, +) { companion object { private const val NO_ELEMENT: Byte = -1 } - private val firstChild: ByteArrayList = ByteArrayList() - private val nextChild: ByteArrayList = ByteArrayList() - private val payload: ByteArrayList = ByteArrayList() - private val data: ArrayList = ArrayList() - - init { + constructor(rootPayload: Byte, rootData: T) : this(ByteArrayList(), ByteArrayList(), ByteArrayList(), ArrayList()) { payload.add(rootPayload) data.add(rootData) firstChild.add(NO_ELEMENT) @@ -101,4 +106,49 @@ class TinyTree(rootPayload: Byte, rootData: T) { get() = payload.size class TooManyElementsException : Exception() + + abstract class Externalizer : VersionedExternalizer> { + + companion object { + // increment on format changed + private const val SERDE_VERSION = 0 + } + + override fun serdeVersion(): Int = SERDE_VERSION + + abstract fun writeDataPayload(output: DataOutput, payload: T) + + abstract fun readDataPayload(input: DataInput): T + + override fun save(output: DataOutput, tree: TinyTree) { + writeINT(output, tree.size) + writeByteArray(output, tree.firstChild) + writeByteArray(output, tree.nextChild) + writeByteArray(output, tree.payload) + for (dataPayload in tree.data) { + writeDataPayload(output, dataPayload) + } + } + + override fun read(input: DataInput): TinyTree { + val size = readINT(input) + val firstChild = readByteArray(input, size) + val nextChild = readByteArray(input, size) + val payload = readByteArray(input, size) + val data = ArrayList(size) + repeat(size) { + data.add(readDataPayload(input)) + } + return TinyTree(firstChild, nextChild, payload, data) + } + + private fun writeByteArray(output: DataOutput, byteArray: ByteArrayList) { + output.write(byteArray.elements(), 0, byteArray.size) + } + + private fun readByteArray(input: DataInput, size: Int): ByteArrayList { + val bytes = ByteArray(size).also { input.readFully(it) } + return ByteArrayList(bytes) + } + } } \ No newline at end of file diff --git a/platform/lang-impl/src/com/intellij/openapi/fileEditor/impl/text/foldingGrave/FoldingState.kt b/platform/lang-impl/src/com/intellij/openapi/fileEditor/impl/text/foldingGrave/FoldingState.kt index 10639ee51d67..c3fa84bd0dc0 100644 --- a/platform/lang-impl/src/com/intellij/openapi/fileEditor/impl/text/foldingGrave/FoldingState.kt +++ b/platform/lang-impl/src/com/intellij/openapi/fileEditor/impl/text/foldingGrave/FoldingState.kt @@ -8,8 +8,8 @@ import com.intellij.openapi.editor.FoldRegion import com.intellij.openapi.editor.ex.FoldingModelEx import com.intellij.openapi.editor.impl.FoldingModelImpl import com.intellij.openapi.editor.impl.FoldingModelImpl.ZOMBIE_REGION_KEY -import com.intellij.openapi.fileEditor.impl.text.TextEditorCache import com.intellij.openapi.fileEditor.impl.text.TextEditorCache.Companion.contentHash +import com.intellij.openapi.fileEditor.impl.text.VersionedExternalizer import com.intellij.util.concurrency.annotations.RequiresEdt import com.intellij.util.io.DataInputOutputUtil.readINT import com.intellij.util.io.DataInputOutputUtil.writeINT @@ -80,7 +80,7 @@ internal class FoldingState(private val contentHash: Int, private val regions: L } } - object FoldingStateExternalizer : TextEditorCache.ValueExternalizer { + object FoldingStateExternalizer : VersionedExternalizer { override fun serdeVersion() = 2 override fun save(output: DataOutput, value: FoldingState) = value.save(output) override fun read(input: DataInput) = FoldingState.read(input) diff --git a/platform/lang-impl/testSources/com/intellij/codeInsight/hints/declarative/TinyTreeTest.kt b/platform/lang-impl/testSources/com/intellij/codeInsight/hints/declarative/TinyTreeTest.kt index a8fb345b12ee..6082a191e679 100644 --- a/platform/lang-impl/testSources/com/intellij/codeInsight/hints/declarative/TinyTreeTest.kt +++ b/platform/lang-impl/testSources/com/intellij/codeInsight/hints/declarative/TinyTreeTest.kt @@ -4,11 +4,16 @@ package com.intellij.codeInsight.hints.declarative import com.intellij.codeInsight.hints.declarative.impl.util.TinyTree import com.intellij.testFramework.UsefulTestCase import junit.framework.TestCase +import java.io.* import com.intellij.codeInsight.hints.declarative.TinyTreeDebugNode as DebugNode class TinyTreeTest : UsefulTestCase() { fun testAddToTreeInReverseOrder() { val tinyTree = createTree() + assertTreeStructure(tinyTree) + } + + private fun assertTreeStructure(tinyTree: TinyTree) { assertEquals(6, tinyTree.size) val debugTree = DebugNode.buildDebugTree(tinyTree) val expectedNode = DebugNode(10, "root", mutableListOf( @@ -46,6 +51,17 @@ class TinyTreeTest : UsefulTestCase() { } } + fun testTreeSerde() { + val externalizer = object : TinyTree.Externalizer() { + override fun writeDataPayload(output: DataOutput, payload: String): Unit = output.writeUTF(payload) + override fun readDataPayload(input: DataInput): String = input.readUTF() + } + val baos = ByteArrayOutputStream() + externalizer.save(DataOutputStream(baos), createTree()) + val deserializedTree = externalizer.read(DataInputStream(ByteArrayInputStream(baos.toByteArray()))) + assertTreeStructure(deserializedTree) + } + private fun createTree(): TinyTree { // 10 // 20 30 diff --git a/platform/platform-impl/src/com/intellij/openapi/fileEditor/impl/text/TextEditorCache.kt b/platform/platform-impl/src/com/intellij/openapi/fileEditor/impl/text/TextEditorCache.kt index 6bbca7fcd570..4ba076a15d9d 100644 --- a/platform/platform-impl/src/com/intellij/openapi/fileEditor/impl/text/TextEditorCache.kt +++ b/platform/platform-impl/src/com/intellij/openapi/fileEditor/impl/text/TextEditorCache.kt @@ -18,13 +18,9 @@ abstract class TextEditorCache(private val project: Project, private val scop protected val cache: ManagedCache = createCache() abstract fun namePrefix(): String - abstract fun valueExternalizer(): ValueExternalizer + abstract fun valueExternalizer(): VersionedExternalizer abstract fun useHeapCache(): Boolean - interface ValueExternalizer : DataExternalizer { - fun serdeVersion(): Int - } - companion object { fun Document.contentHash(): Int = Hashing.komihash5_0().hashCharsToInt(this.immutableCharSequence) fun cachePath(): Path = PathManager.getSystemDir().resolve("editor") diff --git a/platform/platform-impl/src/com/intellij/openapi/fileEditor/impl/text/VersionedExternalizer.kt b/platform/platform-impl/src/com/intellij/openapi/fileEditor/impl/text/VersionedExternalizer.kt new file mode 100644 index 000000000000..3e9a92c70704 --- /dev/null +++ b/platform/platform-impl/src/com/intellij/openapi/fileEditor/impl/text/VersionedExternalizer.kt @@ -0,0 +1,10 @@ +// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.openapi.fileEditor.impl.text + +import com.intellij.util.io.DataExternalizer +import org.jetbrains.annotations.ApiStatus + +@ApiStatus.Internal +interface VersionedExternalizer : DataExternalizer { + fun serdeVersion(): Int +} diff --git a/platform/platform-resources/src/META-INF/PlatformLangComponents.xml b/platform/platform-resources/src/META-INF/PlatformLangComponents.xml index 2484cc564daa..54b31698757b 100644 --- a/platform/platform-resources/src/META-INF/PlatformLangComponents.xml +++ b/platform/platform-resources/src/META-INF/PlatformLangComponents.xml @@ -123,6 +123,7 @@ +