IJPL-440 Support editor markup cache for declarative inlay hints

Merge-request: IJ-MR-122603
Merged-by: Aleksandr Trushev <aleksandr.trushev@jetbrains.com>

GitOrigin-RevId: 9e60cec17e5f91cf0a6841761e13a6df59a365a0
This commit is contained in:
Aleksandr Trushev
2023-12-19 16:27:35 +00:00
committed by intellij-monorepo-bot
parent 6ba5b0d436
commit b7cfce2b0a
19 changed files with 665 additions and 105 deletions
@@ -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<DeclarativeHintsGrave>()
val inlayDataList = grave.raise(file, document) ?: return
val psiManager = project.serviceAsync<PsiManager>()
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<DeclarativeHintsState>(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<InlayData>? {
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<Any?>, 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")
}
@@ -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<InlayData>) {
class Externalizer : VersionedExternalizer<DeclarativeHintsState> {
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)
}
}
}
@@ -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<FileMarkupInfo> {
object FileMarkupInfoExternalizer : VersionedExternalizer<FileMarkupInfo> {
override fun serdeVersion() = 2
override fun save(output: DataOutput, value: FileMarkupInfo) = value.bury(output)
override fun read(input: DataInput) = FileMarkupInfo.exhume(input)
@@ -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<Code
Pair(TextRange(entry.startOffset, entry.endOffset), entry.asZombie())
}
object Externalizer : TextEditorCache.ValueExternalizer<CodeVisionState> {
object Externalizer : VersionedExternalizer<CodeVisionState> {
override fun serdeVersion() = 0
override fun save(output: DataOutput, value: CodeVisionState) {
@@ -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<Pair<Int, HintData>>) {
object Externalizer : TextEditorCache.ValueExternalizer<ParameterHintsState> {
object Externalizer : VersionedExternalizer<ParameterHintsState> {
override fun serdeVersion() = 0
override fun save(output: DataOutput, value: ParameterHintsState) {
@@ -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<SmartList<Inlay<out DeclarativeInlayRenderer>>>() // either inlay or list of inlays
val offsetToExistingEolElements = Int2ObjectOpenHashMap<SmartList<Inlay<out DeclarativeInlayRenderer>>>() // 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<InlayData>) {
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<SmartList<Inlay<out DeclarativeInlayRenderer>>>() // either inlay or list of inlays
val offsetToExistingEolElements = Int2ObjectOpenHashMap<SmartList<Inlay<out DeclarativeInlayRenderer>>>() // 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<SmartList<Inlay<out DeclarativeInlayRenderer>>>) {
for (inlays in offsetToExistingInlays.values) {
for (inlay in inlays) {
Disposer.dispose(inlay)
private fun deleteNotPreservedInlays(offsetToExistingInlays: Int2ObjectOpenHashMap<SmartList<Inlay<out DeclarativeInlayRenderer>>>) {
for (inlays in offsetToExistingInlays.values) {
for (inlay in inlays) {
Disposer.dispose(inlay)
}
}
}
}
private fun tryUpdateAndDeleteFromListInlay(offsetToExistingInlays: Int2ObjectOpenHashMap<SmartList<Inlay<out DeclarativeInlayRenderer>>>,
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<SmartList<Inlay<out DeclarativeInlayRenderer>>>,
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? {
@@ -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<DeclarativeInlayRenderer>? = null
@@ -89,5 +91,8 @@ class DeclarativeInlayRenderer(
override fun getContextMenuGroupId(inlay: Inlay<*>): String {
return "DummyActionGroup"
}
}
internal fun toInlayData(): InlayData {
return presentationList.toInlayData(position, providerId)
}
}
@@ -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<Any?>,
val providerId: String,
val disabled: Boolean,
val payloads: List<InlayPayload>?,
/**
* Just for debugging purposes
*/
val providerClass: Class<*>,
) {
class Externalizer : VersionedExternalizer<InlayData> {
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<Any?> = treeExternalizer.read(input)
val providerId: String = readUTF(input)
val disabled: Boolean = input.readBoolean()
val payloads: List<InlayPayload>? = 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<InlayPayload>?) {
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<InlayPayload>? {
if (input.readBoolean()) {
val payloadCount = readINT(input)
val payloads = ArrayList<InlayPayload>(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)
}
}
}
@@ -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,
)
}
}
@@ -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<Any?>,
val providerId: String,
val disabled: Boolean,
val payloads: List<InlayPayload>?,
/**
* Just for debugging purposes
*/
val providerClass: Class<*>
) {
}
@@ -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<Any?>, 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<Any?>, 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
@@ -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<Any?>() {
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
}
}
}
@@ -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<PsiElement> {
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
}
@@ -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<T>(rootPayload: Byte, rootData: T) {
class TinyTree<T> private constructor(
private val firstChild: ByteArrayList,
private val nextChild: ByteArrayList,
private val payload: ByteArrayList,
private val data: ArrayList<T>,
) {
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<T> = 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<T>(rootPayload: Byte, rootData: T) {
get() = payload.size
class TooManyElementsException : Exception()
abstract class Externalizer<T> : VersionedExternalizer<TinyTree<T>> {
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<T>) {
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<T> {
val size = readINT(input)
val firstChild = readByteArray(input, size)
val nextChild = readByteArray(input, size)
val payload = readByteArray(input, size)
val data = ArrayList<T>(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)
}
}
}
@@ -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<FoldingState> {
object FoldingStateExternalizer : VersionedExternalizer<FoldingState> {
override fun serdeVersion() = 2
override fun save(output: DataOutput, value: FoldingState) = value.save(output)
override fun read(input: DataInput) = FoldingState.read(input)
@@ -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<String>) {
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<String>() {
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<String> {
// 10
// 20 30
@@ -18,13 +18,9 @@ abstract class TextEditorCache<T>(private val project: Project, private val scop
protected val cache: ManagedCache<Int, T> = createCache()
abstract fun namePrefix(): String
abstract fun valueExternalizer(): ValueExternalizer<T>
abstract fun valueExternalizer(): VersionedExternalizer<T>
abstract fun useHeapCache(): Boolean
interface ValueExternalizer<T> : DataExternalizer<T> {
fun serdeVersion(): Int
}
companion object {
fun Document.contentHash(): Int = Hashing.komihash5_0().hashCharsToInt(this.immutableCharSequence)
fun cachePath(): Path = PathManager.getSystemDir().resolve("editor")
@@ -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<T> : DataExternalizer<T> {
fun serdeVersion(): Int
}
@@ -123,6 +123,7 @@
<textEditorInitializer implementation="com.intellij.openapi.fileEditor.impl.text.FoldingTextEditorInitializer"/>
<textEditorInitializer implementation="com.intellij.codeInsight.hints.codeVision.CodeVisionTextEditorInitializer"/>
<textEditorInitializer implementation="com.intellij.codeInsight.daemon.impl.InlayHintsEditorInitializer"/>
<textEditorInitializer implementation="com.intellij.codeInsight.daemon.impl.DeclarativeHintsEditorInitializer"/>
<textEditorInitializer implementation="com.intellij.codeInsight.hints.ParameterHintsEditorInitializer"/>
<textEditorInitializer implementation="com.intellij.openapi.fileEditor.impl.text.DocRenderTextEditorInitializer"/>
<textEditorInitializer implementation="com.intellij.openapi.fileEditor.impl.text.FocusZoneTextEditorInitializer"/>