[declarative-inlays] IJPL-164727 support for remote clients

GitOrigin-RevId: d284550a00efea6b0f1e26bb1e27328499e115b3
This commit is contained in:
Vojtech Balik
2025-02-22 18:10:11 +00:00
committed by intellij-monorepo-bot
parent 06b0f857b5
commit 6c70af79a4
14 changed files with 145 additions and 29 deletions
@@ -4,6 +4,7 @@ package com.intellij.codeInsight.hints.declarative
import com.intellij.model.Pointer
import com.intellij.model.Symbol
import com.intellij.psi.SmartPsiElementPointer
import org.jetbrains.annotations.ApiStatus
/**
* Once the tree building started, it must provide at least one text node. Otherwise, an exception will be thrown.
@@ -58,7 +59,8 @@ class InlayActionData(val payload: InlayActionPayload, val handlerId: String) {
}
}
sealed interface InlayActionPayload
@ApiStatus.NonExtendable
interface InlayActionPayload
class StringInlayActionPayload(val text: String) : InlayActionPayload {
override fun equals(other: Any?): Boolean {
+3 -6
View File
@@ -3918,12 +3918,6 @@ f:com.intellij.codeInsight.hints.declarative.impl.DeclarativeInlayHintsPassFacto
- f:getSuitableToFileProviders(com.intellij.psi.PsiFile):java.util.List
- f:scheduleRecompute(com.intellij.openapi.editor.Editor,com.intellij.openapi.project.Project):V
- f:updateModificationStamp(com.intellij.openapi.editor.Editor,com.intellij.psi.PsiFile):V
f:com.intellij.codeInsight.hints.declarative.impl.InlayTreeSinkImpl
- com.intellij.codeInsight.hints.declarative.InlayTreeSink
- <init>(java.lang.String,java.util.Map,Z,Z,java.lang.Class,java.lang.String):V
- addPresentation(com.intellij.codeInsight.hints.declarative.InlayPosition,java.util.List,java.lang.String,com.intellij.codeInsight.hints.declarative.HintFormat,kotlin.jvm.functions.Function1):V
- f:finish():java.util.List
- whenOptionEnabled(java.lang.String,kotlin.jvm.functions.Function0):V
f:com.intellij.codeInsight.hints.declarative.impl.util.TinyTree
- sf:Companion:com.intellij.codeInsight.hints.declarative.impl.util.TinyTree$Companion
- <init>(B,java.lang.Object):V
@@ -3931,9 +3925,12 @@ f:com.intellij.codeInsight.hints.declarative.impl.util.TinyTree
- f:getBytePayload(B):B
- f:getDataPayload(B):java.lang.Object
- f:getSize():I
- f:isSameAs(com.intellij.codeInsight.hints.declarative.impl.util.TinyTree,kotlin.jvm.functions.Function2,kotlin.jvm.functions.Function2):Z
- bs:isSameAs$default(com.intellij.codeInsight.hints.declarative.impl.util.TinyTree,com.intellij.codeInsight.hints.declarative.impl.util.TinyTree,kotlin.jvm.functions.Function2,kotlin.jvm.functions.Function2,I,java.lang.Object):Z
- f:processChildren(B,kotlin.jvm.functions.Function1):V
- f:reverseChildren():V
- f:setBytePayload(B,B):V
- f:setDataPayload(java.lang.Object,B):V
- f:syncProcessChildren(B,B,com.intellij.codeInsight.hints.declarative.impl.util.TinyTree,kotlin.jvm.functions.Function2):V
f:com.intellij.codeInsight.hints.declarative.impl.util.TinyTree$Companion
a:com.intellij.codeInsight.hints.declarative.impl.util.TinyTree$Externalizer
@@ -49,6 +49,7 @@ private object NecromancyPresentationTreeExternalizer : PresentationTreeExternal
is SymbolPointerInlayActionPayload -> {
writeINT(output, 2)
}
else -> { /* do nothing */ }
}
}
@@ -278,7 +278,8 @@ object InlayHintsUtils {
private val TEXT_METRICS_STORAGE = Key.create<InlayTextMetricsStorage>("InlayTextMetricsStorage")
internal fun getTextMetricStorage(editor: Editor): InlayTextMetricsStorage {
@ApiStatus.Internal
fun getTextMetricStorage(editor: Editor): InlayTextMetricsStorage {
val storage = editor.getUserData(TEXT_METRICS_STORAGE)
if (storage == null) {
val newStorage = InlayTextMetricsStorage(editor)
@@ -28,7 +28,6 @@ open class DeclarativeInlayActionService {
val inlayMenu: AnAction = ActionManager.getInstance().getAction("InlayMenu")
val inlayMenuActionGroup = inlayMenu as ActionGroup
//val popupMenu = ActionManager.getInstance().createActionPopupMenu("InlayMenuPopup", inlayMenuActionGroup)
val dataContext = SimpleDataContext.builder()
.add(CommonDataKeys.PROJECT, project)
.add(CommonDataKeys.PSI_FILE, psiFile)
@@ -37,11 +36,6 @@ open class DeclarativeInlayActionService {
.add(InlayHintsProvider.PROVIDER_NAME, providerName)
.add(InlayHintsProvider.INLAY_PAYLOADS, hintData.payloads?.associate { it.payloadName to it.payload })
.build()
//popupMenu.setDataContext {
// dataContext
//}
//
//JBPopupMenu.showByEvent(e.mouseEvent, popupMenu.component)
val popupMenu = JBPopupFactory.getInstance().createActionGroupPopup(null, inlayMenuActionGroup, dataContext, JBPopupFactory.ActionSelectionAid.SPEEDSEARCH, false)
popupMenu.show(relativePoint)
@@ -45,7 +45,8 @@ class DeclarativeInlayHintsPass(
continue
}
val sink = InlayTreeSinkImpl(providerInfo.providerId, providerInfo.optionToEnabled, isPreview, isProviderDisabled, provider.javaClass, passSourceId)
val sink = InlayTreeSinkImpl(providerInfo.providerId, providerInfo.optionToEnabled, isPreview, isProviderDisabled,
provider.javaClass, passSourceId)
sinks.add(sink)
when (val collector = createCollector(provider)) {
is OwnBypassCollector -> ownCollectors.add(CollectionInfo(sink, collector))
@@ -120,6 +121,7 @@ class DeclarativeInlayHintsPass(
groupKey = { inlay -> document.getLineNumber(inlay.offset) }
)
val storage = InlayHintsUtils.getTextMetricStorage(editor)
val publisher = project.messageBus.syncPublisher(DeclarativeInlayUpdateListener.TOPIC)
fun ensureConsistentSourceId(inlayData: InlayData) {
if (inlayData.sourceId != sourceId) {
@@ -132,7 +134,8 @@ class DeclarativeInlayHintsPass(
val lineEndOffset = editor.document.getLineEndOffset(position.line)
val updated = tryUpdateInlayAndRemoveFromDeleteList(
offsetToExistingEolInlays, inlayData, lineEndOffset,
require = { inlay -> inlay.renderer.providerId == inlayData.providerId }
require = { inlay -> inlay.renderer.providerId == inlayData.providerId },
afterModelUpdate = { inlay, oldModel, newModel -> publisher.afterModelUpdate(inlay, oldModel, listOf(newModel)) }
)
if (!updated) {
val renderer = DeclarativeInlayRenderer(inlayData, storage, inlayData.providerId, sourceId)
@@ -151,7 +154,8 @@ class DeclarativeInlayHintsPass(
val position = inlayData.position as InlineInlayPosition
val updated = tryUpdateInlayAndRemoveFromDeleteList(
offsetToExistingInlineInlays, inlayData, position.offset,
require = { inlay -> inlay.renderer.providerId == inlayData.providerId }
require = { inlay -> inlay.renderer.providerId == inlayData.providerId && inlay.isRelatedToPrecedingText == position.relatedToPrevious },
afterModelUpdate = { inlay, oldModel, newModel -> publisher.afterModelUpdate(inlay, oldModel, listOf(newModel)) }
)
if (!updated) {
val renderer = DeclarativeInlayRenderer(inlayData, storage, inlayData.providerId, sourceId)
@@ -165,7 +169,8 @@ class DeclarativeInlayHintsPass(
inlayData.forEach { ensureConsistentSourceId(it) }
val updated = tryUpdateInlayAndRemoveFromDeleteList(
offsetToExistingBlockInlays, inlayData, line,
require = { inlay -> inlay.renderer.providerId == providerId && inlay.properties.priority == verticalPriority }
require = { inlay -> inlay.renderer.providerId == providerId && inlay.properties.priority == verticalPriority },
afterModelUpdate = { inlay, oldModel, newModel -> publisher.afterModelUpdate(inlay, oldModel, newModel) }
)
if (!updated) {
val renderer = DeclarativeIndentedBlockInlayRenderer(
@@ -195,19 +200,22 @@ class DeclarativeInlayHintsPass(
}
}
private fun <M> tryUpdateInlayAndRemoveFromDeleteList(
/** @return `true` if a suitable inlay was found and updated; `false` otherwise. */
private inline fun <M> tryUpdateInlayAndRemoveFromDeleteList(
offsetToExistingInlays: Int2ObjectOpenHashMap<out SmartList<out Inlay<out DeclarativeInlayRendererBase<M>>>>,
inlayData: M,
groupKey: Int,
require: (Inlay<out DeclarativeInlayRendererBase<M>>) -> Boolean
require: (Inlay<out DeclarativeInlayRendererBase<M>>) -> Boolean,
afterModelUpdate: (existingInlay: Inlay<out DeclarativeInlayRendererBase<M>>, oldModel: List<InlayData>, newModel: M) -> Unit
): Boolean {
val inlays = offsetToExistingInlays.get(groupKey)
if (inlays == null) return false
val inlays = offsetToExistingInlays.get(groupKey) ?: return false
val iterator = inlays.iterator()
while (iterator.hasNext()) {
val existingInlay = iterator.next()
if (require(existingInlay)) {
val oldInlayData = existingInlay.renderer.toInlayData(false)
existingInlay.renderer.updateModel(inlayData)
afterModelUpdate(existingInlay, oldInlayData, inlayData)
existingInlay.update()
iterator.remove()
return true
@@ -250,7 +258,12 @@ class DeclarativeInlayHintsPass(
)
}
private inline fun List<AboveLineIndentedPositionDetail>.forEachRun(action: (Int, String, Int, List<InlayData>) -> Unit) {
private inline fun List<AboveLineIndentedPositionDetail>.forEachRun(
action: (line: Int,
providerId: String,
verticalPriority: Int,
inlayData: List<InlayData>) -> Unit
) {
if (isEmpty()) return
// run ⇔ group of inlays with the same line, provider and vertical priority
// preprocessedInlayData.aboveLine are sorted by these properties
@@ -0,0 +1,26 @@
// Copyright 2000-2024 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.impl.inlayRenderer.DeclarativeInlayRendererBase
import com.intellij.openapi.editor.Inlay
import com.intellij.util.messages.Topic
import org.jetbrains.annotations.ApiStatus
import java.util.EventListener
/**
* This listener exists, because [com.intellij.openapi.editor.InlayModel.Listener.onUpdated]
* might not be fired for some declarative inlay updates. (For example, it is not fired when only the tooltip changes).
*/
@ApiStatus.Internal
interface DeclarativeInlayUpdateListener : EventListener {
companion object {
@JvmField
@Topic.ProjectLevel
val TOPIC: Topic<DeclarativeInlayUpdateListener> = Topic(DeclarativeInlayUpdateListener::class.java, Topic.BroadcastDirection.NONE)
}
/** Both [oldModel] and [newModel] are the same as the result
* of calling [inlay.renderer.toInlayData(needsUpToDateOffsets = false)][DeclarativeInlayRendererBase.toInlayData]
* before and after the model update respectively. */
fun afterModelUpdate(inlay: Inlay<out DeclarativeInlayRendererBase<*>>, oldModel: List<InlayData>, newModel: List<InlayData>)
}
@@ -15,6 +15,7 @@ import com.intellij.openapi.editor.event.EditorMouseEvent
import com.intellij.openapi.editor.impl.EditorImpl
import com.intellij.openapi.editor.markup.TextAttributes
import com.intellij.ui.LightweightHint
import com.intellij.ui.awt.RelativePoint
import com.intellij.util.SlowOperations
import com.intellij.util.concurrency.annotations.RequiresEdt
import com.intellij.util.containers.enumMapOf
@@ -84,7 +85,7 @@ class InlayPresentationList(
pointInsideInlay: Point,
fontMetricsStorage: InlayTextMetricsStorage,
) {
service<DeclarativeInlayActionService>().invokeInlayMenu(model, e)
service<DeclarativeInlayActionService>().invokeInlayMenu(model, e, RelativePoint(e.mouseEvent.locationOnScreen))
}
private val marginAndPadding: Pair<Int, Int> get() = MARGIN_PADDING_BY_FORMAT[model.hintFormat.horizontalMarginPadding]!!
@@ -4,6 +4,7 @@ package com.intellij.codeInsight.hints.declarative.impl
import com.intellij.codeInsight.hints.declarative.*
import com.intellij.diagnostic.PluginException
import com.intellij.openapi.util.NlsContexts
import org.jetbrains.annotations.ApiStatus
/**
@@ -13,6 +14,7 @@ import com.intellij.openapi.util.NlsContexts
* @param enabledOptions an exhaustive set of options
* @param providerClass used for diagnostics only
*/
@ApiStatus.Internal
class InlayTreeSinkImpl(
private val providerId: String,
private val enabledOptions: Map<String, Boolean>,
@@ -151,4 +151,4 @@ private class InlayTreeBuildingContext(private val position: InlayPosition) {
}
fun isTruncateTextNodes() = position is InlineInlayPosition
}
}
@@ -14,6 +14,8 @@ import java.io.DataOutput
@ApiStatus.Internal
object RdInlayDataExternalizer : InlayDataExternalizer(RdPresentationTreeExternalizer) {
const val RD_INLAY_DATA_SOURCE_ID: String = "rd.deserialized.inlay.data"
override fun writeProviderClass(output: DataOutput, providerClass: Class<*>) {
// do nothing
}
@@ -66,5 +68,3 @@ private object RdDummyInlayProvider : InlayHintsProvider {
error("Dummy provider must not be called")
}
}
const val RD_INLAY_DATA_SOURCE_ID: String = "rd.deserialized.inlay.data"
@@ -38,7 +38,8 @@ abstract class DeclarativeInlayRendererBase<Model>(
abstract val presentationLists: List<InlayPresentationList>
@RequiresEdt
internal fun updateModel(newModel: Model) {
@ApiStatus.Internal
fun updateModel(newModel: Model) {
view.updateModel(newModel)
}
@@ -72,7 +73,8 @@ abstract class DeclarativeInlayRendererBase<Model>(
return "DummyActionGroup"
}
internal fun toInlayData(needUpToDateOffsets: Boolean = true): List<InlayData> {
@ApiStatus.Internal
fun toInlayData(needUpToDateOffsets: Boolean = true): List<InlayData> {
// this.inlay should always be initialized right after construction.
// However, InlayModel.Listener.onAdded will be called before that can happen,
// and someone (e.g., rem-dev backend) might want to serialize right away;
@@ -0,0 +1,60 @@
// Copyright 2000-2024 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.codeInsight.hints.declarative.*
import com.intellij.codeInsight.hints.declarative.impl.ActionWithContent
import com.intellij.openapi.editor.Inlay
import org.jetbrains.annotations.ApiStatus
@ApiStatus.Internal
object DeclarativeInlayHintsUtil {
fun InlayPosition.getPlacement(): Inlay.Placement =
when (this) {
is AboveLineIndentedPosition -> Inlay.Placement.ABOVE_LINE
is EndOfLinePosition -> Inlay.Placement.AFTER_LINE_END
is InlineInlayPosition -> Inlay.Placement.INLINE
}
fun InlayPosition.getRelatesToPrecedingText(): Boolean =
when (this) {
is AboveLineIndentedPosition -> false
is EndOfLinePosition -> true
is InlineInlayPosition -> relatedToPrevious
}
/** @see com.intellij.openapi.editor.InlayProperties.getPriority */
fun InlayPosition.getInlayPriority(): Int =
when (this) {
is AboveLineIndentedPosition -> verticalPriority
is EndOfLinePosition -> priority
is InlineInlayPosition -> priority
}
/** Transform [InlayActionData] stored inside a declarative inlay tree node at given index */
inline fun TinyTree<Any?>.mapInlayActionData(index: Byte, transform: (InlayActionData) -> InlayActionData) {
val dataPayload = getDataPayload(index)
if (dataPayload is ActionWithContent) {
val oldActionData = dataPayload.actionData
val newActionData = transform(oldActionData)
if (oldActionData !== newActionData) {
setDataPayload(
ActionWithContent(
newActionData,
dataPayload.content),
index
)
}
}
else if (dataPayload is InlayActionData) {
val oldActionData = dataPayload
val newActionData = transform(oldActionData)
if (oldActionData !== newActionData) {
setDataPayload(
newActionData,
index
)
}
}
// else: not a node with InlayActionData, do nothing
}
}
@@ -77,6 +77,10 @@ class TinyTree<T> private constructor(
payload.set(index.toInt(), nodePayload)
}
fun setDataPayload(data: T, index: Byte) {
this.data[index.toInt()] = data
}
fun processChildren(index: Byte, f: (index: Byte) -> Boolean) {
var currentChildIndex = firstChild.getByte(index.toInt())
while (currentChildIndex != NO_ELEMENT) {
@@ -102,6 +106,19 @@ class TinyTree<T> private constructor(
}
}
fun isSameAs(
other: TinyTree<T>,
isPayloadSame: (Byte, Byte) -> Boolean = { a, b -> a == b },
isDataSame: (T, T) -> Boolean = { a, b -> a == b },
): Boolean {
if (size != other.size) return false
if (firstChild != other.firstChild) return false
if (nextChild != other.nextChild) return false
if (!payload.indices.all { isPayloadSame(payload.getByte(it), other.payload.getByte(it)) }) return false
if (!data.indices.all { isDataSame(data[it], other.data[it]) }) return false
return true
}
val size: Int
get() = payload.size