[declarative-inlays] refactoring

* simplify inlay presentation hierarchy
* extract inlay mouse interactions into a separate interface
* allow margins for presentation entries

GitOrigin-RevId: f0562a1bc933e6a6150135d804def60db2bb41f8
This commit is contained in:
Vojtech Balik
2025-06-02 21:49:30 +02:00
committed by intellij-monorepo-bot
parent 897108f068
commit cfb2e2fe61
22 changed files with 763 additions and 790 deletions

View File

@@ -3736,8 +3736,11 @@ f:com.intellij.codeInsight.hints.presentation.InlayTextMetrics
- f:getFont():java.awt.Font
- f:getFontBaseline():I
- f:getFontHeight():I
- f:getFontMetrics():java.awt.FontMetrics
- f:getFontType():I
- f:getIdeScale():F
- f:getLineHeight():I
- f:getSpaceWidth():I
- f:getStringWidth(java.lang.String):I
- f:isActual(F,java.lang.String):Z
- f:offsetFromTop():I

View File

@@ -53,4 +53,6 @@ open class DeclarativeInlayActionService {
open fun logActionHandlerInvoked(handlerId: String, handlerClass: Class<out InlayActionHandler>) {
InlayActionHandlerUsagesCollector.clickHandled(handlerId, handlerClass)
}
// TODO see if showTooltip/hideTooltip can be extracted to here
}

View File

@@ -2,27 +2,132 @@
package com.intellij.codeInsight.hints.declarative.impl.inlayRenderer
import com.intellij.codeInsight.hints.declarative.impl.InlayData
import com.intellij.codeInsight.hints.declarative.impl.views.IndentedDeclarativeHintView
import com.intellij.codeInsight.hints.declarative.impl.views.MultipleDeclarativeHintsView
import com.intellij.codeInsight.hints.declarative.impl.views.CapturedPointInfo
import com.intellij.codeInsight.hints.declarative.impl.views.InlayPresentationComposite
import com.intellij.codeInsight.hints.declarative.impl.views.InlayPresentationList
import com.intellij.codeInsight.hints.presentation.InlayTextMetricsStorage
import com.intellij.formatting.visualLayer.VirtualFormattingInlaysInfo
import com.intellij.openapi.editor.Document
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.Inlay
import com.intellij.openapi.editor.ex.util.EditorUtil
import com.intellij.openapi.editor.markup.TextAttributes
import com.intellij.util.DocumentUtil
import com.intellij.util.concurrency.annotations.RequiresReadLock
import com.intellij.util.text.CharArrayUtil
import org.jetbrains.annotations.ApiStatus
import java.awt.Graphics2D
import java.awt.Point
import java.awt.geom.Rectangle2D
/**
* Indents the hint to match the indent of the line given by the offset of the carrying inlay
*
* Caveats:
* - Calculating the width of the indent requires a read action during rendering.
* More correct approach would be to calculate and update it in [com.intellij.codeInsight.hints.declarative.impl.DeclarativeInlayHintsPass],
* however, that creates a noticeable delay between when, e.g., user increases the indent of the line and the time the indent of the hint
* updates.
* - [Inlay.getOffset] will not return the correct value during inlay construction,
* which is when `calcWidthInPixels` will be called for the first time. `initialIndentAnchorOffset` is used as a work-around.
*/
@ApiStatus.Internal
class DeclarativeIndentedBlockInlayRenderer(
inlayData: List<InlayData>,
fontMetricsStorage: InlayTextMetricsStorage,
providerId: String,
sourceId: String,
initialIndentAnchorOffset: Int,
private val initialIndentAnchorOffset: Int,
) : DeclarativeInlayRendererBase<List<InlayData>>(providerId, sourceId, fontMetricsStorage) {
override val view = IndentedDeclarativeHintView(MultipleDeclarativeHintsView(inlayData), initialIndentAnchorOffset)
override val view = InlayPresentationComposite(inlayData)
override val presentationLists get() = view.view.presentationLists
override val presentationLists: List<InlayPresentationList> get() = view.presentationLists
override fun initInlay(inlay: Inlay<out DeclarativeInlayRendererBase<List<InlayData>>>) {
super.initInlay(inlay)
view.inlay = inlay
override fun updateModel(newModel: List<InlayData>) {
view.updateModel(newModel)
}
override fun paint(inlay: Inlay<*>, g: Graphics2D, targetRegion: Rectangle2D, textAttributes: TextAttributes) {
super.paint(inlay, g, targetRegion.toViewRectangle(), textAttributes)
}
override fun calcWidthInPixels(inlay: Inlay<*>): Int {
// ignore margins here:
// * left-margin is subsumed by the indent margin (even if indent is 0, so that the edges are aligned),
// * right-margin is not needed as there will be nothing displayed to the left of the block inlay
return getViewIndentMargin(inlay) + view.getSubViewMetrics(textMetricsStorage).boxWidth
}
override fun capturePoint(pointInsideInlay: Point, textMetricsStorage: InlayTextMetricsStorage): CapturedPointInfo? {
return super.capturePoint(pointInsideInlay.toPointInsideViewOrNull() ?: return null, textMetricsStorage)
}
private fun Point.toPointInsideViewOrNull(): Point? {
val indentMargin = getViewIndentMargin()
if (this.x < indentMargin) {
return null
}
return Point(this.x - indentMargin, this.y)
}
private fun Rectangle2D.toViewRectangle(): Rectangle2D {
val indentMargin = getViewIndentMargin()
return Rectangle2D.Double(this.x + indentMargin,
this.y,
this.width - indentMargin,
this.height)
}
private fun getViewIndentMargin(inlay: Inlay<*>? = null): Int =
if (this::inlay.isInitialized) {
calcViewIndentMargin(this.inlay.offset, this.inlay.editor)
}
else if (inlay != null) {
calcViewIndentMargin(initialIndentAnchorOffset, inlay.editor)
}
else {
0
}
}
@RequiresReadLock
private fun calcViewIndentMargin(offset: Int, editor: Editor): Int {
val document = editor.document
val text = document.immutableCharSequence
val (lineStartOffset, textStartOffset) = calcIndentAnchorOffset(offset, document)
val indentMargin = if (editor.inlayModel.isInBatchMode) {
// avoid coordinate transformations in batch mode
measureIndentSafely(text, lineStartOffset, textStartOffset, editor)
}
else {
val vfmtRightShift = VirtualFormattingInlaysInfo.measureVirtualFormattingInlineInlays(editor, textStartOffset, textStartOffset)
editor.offsetToXY(textStartOffset, false, false).x + vfmtRightShift
}
return indentMargin
}
private fun measureIndentSafely(text: CharSequence, start: Int, end: Int, editor: Editor): Int {
val spaceWidth = EditorUtil.getPlainSpaceWidth(editor)
val tabSize = EditorUtil.getTabSize(editor)
var columns = 0
var offset = start
while (offset < end) {
val c = text[offset++]
when (c) {
'\t' -> {
columns += (columns / tabSize + 1) * tabSize
}
else -> {
columns += 1
}
}
}
return columns * spaceWidth
}
private fun calcIndentAnchorOffset(offset: Int, document: Document): Pair<Int, Int> {
val lineStartOffset = DocumentUtil.getLineStartOffset(offset, document)
val textStartOffset = CharArrayUtil.shiftForward(document.immutableCharSequence, lineStartOffset, " \t")
return lineStartOffset to textStartOffset
}

View File

@@ -3,7 +3,6 @@ package com.intellij.codeInsight.hints.declarative.impl.inlayRenderer
import com.intellij.codeInsight.hints.declarative.impl.InlayData
import com.intellij.codeInsight.hints.declarative.impl.views.InlayPresentationList
import com.intellij.codeInsight.hints.declarative.impl.views.SingleDeclarativeHintView
import com.intellij.codeInsight.hints.presentation.InlayTextMetricsStorage
import org.jetbrains.annotations.ApiStatus
import org.jetbrains.annotations.TestOnly
@@ -16,10 +15,13 @@ class DeclarativeInlayRenderer(
sourceId: String,
) : DeclarativeInlayRendererBase<InlayData>(providerId, sourceId, fontMetricsStorage) {
override val view = SingleDeclarativeHintView(inlayData)
override val view = InlayPresentationList(inlayData)
@get:TestOnly
override val presentationLists get() = listOf(presentationList)
override fun updateModel(newModel: InlayData) {
view.updateModel(newModel)
}
@get:TestOnly
val presentationList: InlayPresentationList get() = view.presentationList
val presentationList: InlayPresentationList get() = view
}

View File

@@ -7,8 +7,13 @@ import com.intellij.codeInsight.hints.declarative.InlayPosition
import com.intellij.codeInsight.hints.declarative.InlineInlayPosition
import com.intellij.codeInsight.hints.declarative.impl.InlayData
import com.intellij.codeInsight.hints.declarative.impl.InlayMouseArea
import com.intellij.codeInsight.hints.declarative.impl.interaction.DefaultInlayInteractionHandler
import com.intellij.codeInsight.hints.declarative.impl.interaction.InlayInteractionHandler
import com.intellij.codeInsight.hints.declarative.impl.views.CapturedPointInfo
import com.intellij.codeInsight.hints.declarative.impl.views.InlayTopLevelElement
import com.intellij.codeInsight.hints.declarative.impl.views.InlayPresentationList
import com.intellij.codeInsight.hints.declarative.impl.views.DeclarativeHintView
import com.intellij.codeInsight.hints.declarative.impl.views.computeFullWidth
import com.intellij.codeInsight.hints.presentation.InlayTextMetricsStamp
import com.intellij.codeInsight.hints.presentation.InlayTextMetricsStorage
import com.intellij.openapi.editor.EditorCustomElementRenderer
import com.intellij.openapi.editor.Inlay
@@ -25,47 +30,79 @@ import java.awt.geom.Rectangle2D
abstract class DeclarativeInlayRendererBase<Model>(
val providerId: String,
val sourceId: String,
val fontMetricsStorage: InlayTextMetricsStorage,
val textMetricsStorage: InlayTextMetricsStorage,
) : EditorCustomElementRenderer {
lateinit var inlay: Inlay<out DeclarativeInlayRendererBase<Model>> private set
lateinit var inlay: Inlay<out DeclarativeInlayRendererBase<Model>> protected set
open fun initInlay(inlay: Inlay<out DeclarativeInlayRendererBase<Model>>) {
this.inlay = inlay
}
internal abstract val view: DeclarativeHintView<Model>
internal abstract val view: InlayTopLevelElement<Model>
abstract val presentationLists: List<InlayPresentationList>
@RequiresEdt
@ApiStatus.Internal
fun updateModel(newModel: Model) {
view.updateModel(newModel)
abstract fun updateModel(newModel: Model)
internal fun invalidate() {
view.invalidate()
}
private var inlayTextMetricsStamp: InlayTextMetricsStamp? = null
protected open fun capturePoint(pointInsideInlay: Point, textMetricsStorage: InlayTextMetricsStorage): CapturedPointInfo? {
return view.findEntryAtPoint(pointInsideInlay, textMetricsStorage)
}
private fun invalidateIfTextMetricsChanged() {
val stamp = textMetricsStorage.getCurrentStamp()
if (stamp != inlayTextMetricsStamp) {
invalidate()
inlayTextMetricsStamp = stamp
}
}
override fun calcWidthInPixels(inlay: Inlay<*>): Int {
return view.calcWidthInPixels(inlay, fontMetricsStorage)
invalidateIfTextMetricsChanged()
return view.computeFullWidth(textMetricsStorage)
}
override fun paint(inlay: Inlay<*>, g: Graphics2D, targetRegion: Rectangle2D, textAttributes: TextAttributes) {
view.paint(inlay, g, targetRegion, textAttributes, fontMetricsStorage)
invalidateIfTextMetricsChanged()
view.paint(inlay, g, targetRegion, textAttributes, textMetricsStorage)
}
internal fun handleLeftClick(e: EditorMouseEvent, pointInsideInlay: Point, controlDown: Boolean) {
view.handleLeftClick(e, pointInsideInlay, fontMetricsStorage, controlDown)
fun getInteractionHandler(): InlayInteractionHandler {
return DefaultInlayInteractionHandler
}
@ApiStatus.Internal
fun handleHover(e: EditorMouseEvent, pointInsideInlay: Point): LightweightHint? {
return view.handleHover(e, pointInsideInlay, fontMetricsStorage)
open fun handleLeftClick(e: EditorMouseEvent, pointInsideInlay: Point, controlDown: Boolean) {
invalidateIfTextMetricsChanged()
val clickInfo = capturePoint(pointInsideInlay, textMetricsStorage) ?: return
getInteractionHandler().handleLeftClick(inlay, clickInfo, e, controlDown)
}
internal fun handleRightClick(e: EditorMouseEvent, pointInsideInlay: Point) {
return view.handleRightClick(e, pointInsideInlay, fontMetricsStorage)
@ApiStatus.Internal
open fun handleHover(e: EditorMouseEvent, pointInsideInlay: Point): LightweightHint? {
invalidateIfTextMetricsChanged()
val clickInfo = capturePoint(pointInsideInlay, textMetricsStorage) ?: return null
return getInteractionHandler().handleHover(inlay, clickInfo, e)
}
internal fun getMouseArea(pointInsideInlay: Point): InlayMouseArea? {
return view.getMouseArea(pointInsideInlay, fontMetricsStorage)
internal open fun handleRightClick(e: EditorMouseEvent, pointInsideInlay: Point) {
invalidateIfTextMetricsChanged()
val clickInfo = capturePoint(pointInsideInlay, textMetricsStorage) ?: return
getInteractionHandler().handleRightClick(inlay, clickInfo, e)
}
internal open fun getMouseArea(pointInsideInlay: Point): InlayMouseArea? {
invalidateIfTextMetricsChanged()
return capturePoint(pointInsideInlay, textMetricsStorage)
?.entry
?.clickArea
}
// this should not be shown anywhere, but it is required to show custom menu in com.intellij.openapi.editor.impl.EditorImpl.DefaultPopupHandler.getActionGroup

View File

@@ -0,0 +1,60 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.codeInsight.hints.declarative.impl.interaction
import com.intellij.codeInsight.hints.declarative.impl.DeclarativeInlayActionService
import com.intellij.codeInsight.hints.declarative.impl.inlayRenderer.DeclarativeInlayRendererBase
import com.intellij.codeInsight.hints.declarative.impl.views.CapturedPointInfo
import com.intellij.codeInsight.hints.presentation.PresentationFactory
import com.intellij.openapi.components.service
import com.intellij.openapi.editor.Inlay
import com.intellij.openapi.editor.event.EditorMouseEvent
import com.intellij.ui.LightweightHint
import com.intellij.ui.awt.RelativePoint
import org.jetbrains.annotations.ApiStatus
@ApiStatus.Internal
object DefaultInlayInteractionHandler : InlayInteractionHandler {
override fun handleLeftClick(
inlay: Inlay<out DeclarativeInlayRendererBase<*>>,
clickInfo: CapturedPointInfo,
e: EditorMouseEvent,
controlDown: Boolean,
) {
if (clickInfo.entry == null) return
val entry = clickInfo.entry
val presentationList = clickInfo.presentationList
val editor = e.editor
val project = editor.project
val clickArea = clickInfo.entry.clickArea
if (clickArea != null && project != null) {
val actionData = clickArea.actionData
if (controlDown) {
service<DeclarativeInlayActionService>().invokeActionHandler(actionData, e)
}
}
if (entry.parentIndexToSwitch != (-1).toByte()) {
presentationList.toggleTreeState(entry.parentIndexToSwitch)
inlay.renderer.invalidate()
}
}
override fun handleHover(
inlay: Inlay<out DeclarativeInlayRendererBase<*>>,
clickInfo: CapturedPointInfo,
e: EditorMouseEvent,
): LightweightHint? {
val tooltip = clickInfo.presentationList.model.tooltip ?: return null
return PresentationFactory(e.editor).showTooltip(e.mouseEvent, tooltip)
}
override fun handleRightClick(
inlay: Inlay<out DeclarativeInlayRendererBase<*>>,
clickInfo: CapturedPointInfo,
e: EditorMouseEvent,
) {
val inlayData = clickInfo.presentationList.model
service<DeclarativeInlayActionService>().invokeInlayMenu(inlayData, e, RelativePoint(e.mouseEvent.locationOnScreen))
}
}

View File

@@ -0,0 +1,31 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.codeInsight.hints.declarative.impl.interaction
import com.intellij.codeInsight.hints.declarative.impl.inlayRenderer.DeclarativeInlayRendererBase
import com.intellij.codeInsight.hints.declarative.impl.views.CapturedPointInfo
import com.intellij.openapi.editor.Inlay
import com.intellij.openapi.editor.event.EditorMouseEvent
import com.intellij.ui.LightweightHint
import org.jetbrains.annotations.ApiStatus
@ApiStatus.Internal
interface InlayInteractionHandler {
fun handleLeftClick(
inlay: Inlay<out DeclarativeInlayRendererBase<*>>,
clickInfo: CapturedPointInfo,
e: EditorMouseEvent,
controlDown: Boolean,
)
fun handleHover(
inlay: Inlay<out DeclarativeInlayRendererBase<*>>,
clickInfo: CapturedPointInfo,
e: EditorMouseEvent,
): LightweightHint?
fun handleRightClick(
inlay: Inlay<out DeclarativeInlayRendererBase<*>>,
clickInfo: CapturedPointInfo,
e: EditorMouseEvent,
)
}

View File

@@ -1,142 +0,0 @@
// 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.views
import com.intellij.codeInsight.hints.declarative.AboveLineIndentedPosition
import com.intellij.codeInsight.hints.declarative.EndOfLinePosition
import com.intellij.codeInsight.hints.declarative.InlayPosition
import com.intellij.codeInsight.hints.declarative.InlineInlayPosition
import com.intellij.codeInsight.hints.declarative.impl.InlayData
import com.intellij.codeInsight.hints.declarative.impl.InlayMouseArea
import com.intellij.codeInsight.hints.presentation.InlayTextMetricsStorage
import com.intellij.openapi.editor.Inlay
import com.intellij.openapi.editor.event.EditorMouseEvent
import com.intellij.openapi.editor.markup.TextAttributes
import com.intellij.ui.LightweightHint
import org.jetbrains.annotations.ApiStatus
import java.awt.Graphics2D
import java.awt.Point
import java.awt.Rectangle
import java.awt.geom.Rectangle2D
import java.util.*
@ApiStatus.Internal
abstract class CompositeDeclarativeHintWithMarginsView<Model, SubView, SubViewModel>(ignoreInitialMargin: Boolean)
: DeclarativeHintView<Model>, ViewWithMarginsCompositeBase<SubView>(ignoreInitialMargin)
where SubView : DeclarativeHintView<SubViewModel>,
SubView : ViewWithMargins {
override fun calcWidthInPixels(inlay: Inlay<*>, fontMetricsStorage: InlayTextMetricsStorage): Int {
return getSubViewMetrics(fontMetricsStorage).fullWidth
}
override fun paint(inlay: Inlay<*>, g: Graphics2D, targetRegion: Rectangle2D, textAttributes: TextAttributes, fontMetricsStorage: InlayTextMetricsStorage) {
forEachSubViewBounds(fontMetricsStorage) { subView, leftBound, rightBound ->
val width = rightBound - leftBound
val currentRegion = Rectangle(targetRegion.x.toInt() + leftBound, targetRegion.y.toInt(), width, targetRegion.height.toInt())
subView.paint(inlay, g, currentRegion, textAttributes, fontMetricsStorage)
}
}
override fun handleLeftClick(e: EditorMouseEvent, pointInsideInlay: Point, fontMetricsStorage: InlayTextMetricsStorage, controlDown: Boolean) {
forSubViewAtPoint(pointInsideInlay, fontMetricsStorage) { subView, translated ->
subView.handleLeftClick(e, translated, fontMetricsStorage, controlDown)
}
}
override fun handleHover(e: EditorMouseEvent, pointInsideInlay: Point, fontMetricsStorage: InlayTextMetricsStorage): LightweightHint? {
forSubViewAtPoint(pointInsideInlay, fontMetricsStorage) { subView, translated ->
return subView.handleHover(e, translated, fontMetricsStorage)
}
return null
}
override fun handleRightClick(e: EditorMouseEvent, pointInsideInlay: Point, fontMetricsStorage: InlayTextMetricsStorage) {
forSubViewAtPoint(pointInsideInlay, fontMetricsStorage) { subView, translated ->
subView.handleRightClick(e, translated, fontMetricsStorage)
}
}
override fun getMouseArea(pointInsideInlay: Point, fontMetricsStorage: InlayTextMetricsStorage): InlayMouseArea? {
forSubViewAtPoint(pointInsideInlay, fontMetricsStorage) { subView, translated ->
return subView.getMouseArea(translated, fontMetricsStorage)
}
return null
}
}
private fun CompositeDeclarativeHintWithMarginsView<*, *, InlayData>.createPresentationList(inlayData: InlayData): InlayPresentationList {
return InlayPresentationList(inlayData, this::invalidateComputedSubViewMetrics)
}
internal class SingleDeclarativeHintView(inlayData: InlayData)
: CompositeDeclarativeHintWithMarginsView<InlayData, InlayPresentationList, InlayData>(false) {
val presentationList = createPresentationList(inlayData)
override val subViewCount: Int get() = 1
override fun getSubView(index: Int): InlayPresentationList = presentationList
override fun updateModel(newModel: InlayData) = presentationList.updateModel(newModel)
}
internal class MultipleDeclarativeHintsView(inlayData: List<InlayData>)
: CompositeDeclarativeHintWithMarginsView<List<InlayData>, InlayPresentationList, InlayData>(true) {
var presentationLists: List<InlayPresentationList> =
if (inlayData.size == 1) {
Collections.singletonList(createPresentationList(inlayData[0]))
}
else {
inlayData.map { createPresentationList(it) }
}
private set
override val subViewCount: Int get() = presentationLists.size
override fun getSubView(index: Int): InlayPresentationList = presentationLists[index]
override fun updateModel(newModel: List<InlayData>) {
/*
We have no reliable way to tell if the new hints are the same hints as the ones being updated or not.
It is a trade-off between correctness and efficiency.
Being incorrect means user CollapseState information is not preserved between DeclarativeInlayHintPasses.
(See InlayPresentationList#toggleTreeState)
*/
if (newModel.size == presentationLists.size) {
// Assume same hints
presentationLists.forEachIndexed { index, presentationList -> presentationList.updateModel(newModel[index]) }
return
}
// Different set of hints from the same provider -- try distinguishing hints using their priorities (assuming those are stable).
var oldIndex = 0
var newIndex = 0
val newPresentationLists = ArrayList<InlayPresentationList>(newModel.size)
while (oldIndex < presentationLists.size && newIndex < newModel.size) {
val oldPrio = presentationLists[oldIndex].model.position.priority
val newPrio = newModel[newIndex].position.priority
when {
oldPrio == newPrio -> {
newPresentationLists.add(presentationLists[oldIndex].also { it.updateModel(newModel[newIndex]) })
oldIndex++
newIndex++
}
oldPrio < newPrio -> {
// assume it's more likely a hint was removed rather than some priority would change
oldIndex++
}
else /* oldPrio > newPrio */ -> {
// assume it's more likely a hint was added
newPresentationLists.add(createPresentationList(newModel[newIndex]))
newIndex++
}
}
}
while (newIndex < newModel.size) {
newPresentationLists.add(createPresentationList(newModel[newIndex]))
newIndex++
}
presentationLists = newPresentationLists
invalidateComputedSubViewMetrics()
}
}
private val InlayPosition.priority: Int get() = when(this) {
is AboveLineIndentedPosition -> priority
is EndOfLinePosition -> priority
is InlineInlayPosition -> priority
}

View File

@@ -1,55 +0,0 @@
// 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.views
import com.intellij.codeInsight.hints.declarative.impl.InlayMouseArea
import com.intellij.codeInsight.hints.presentation.InlayTextMetricsStorage
import com.intellij.openapi.editor.Inlay
import com.intellij.openapi.editor.event.EditorMouseEvent
import com.intellij.openapi.editor.markup.TextAttributes
import com.intellij.ui.LightweightHint
import com.intellij.util.concurrency.annotations.RequiresEdt
import org.jetbrains.annotations.ApiStatus
import java.awt.Graphics2D
import java.awt.Point
import java.awt.geom.Rectangle2D
@ApiStatus.Internal
interface DeclarativeHintView<Model> {
@RequiresEdt
fun updateModel(newModel: Model)
@RequiresEdt
fun calcWidthInPixels(inlay: Inlay<*>, fontMetricsStorage: InlayTextMetricsStorage): Int
fun paint(
inlay: Inlay<*>,
g: Graphics2D,
targetRegion: Rectangle2D,
textAttributes: TextAttributes,
fontMetricsStorage: InlayTextMetricsStorage,
)
fun handleLeftClick(
e: EditorMouseEvent,
pointInsideInlay: Point,
fontMetricsStorage: InlayTextMetricsStorage,
controlDown: Boolean,
)
fun handleHover(
e: EditorMouseEvent,
pointInsideInlay: Point,
fontMetricsStorage: InlayTextMetricsStorage,
): LightweightHint?
fun handleRightClick(
e: EditorMouseEvent,
pointInsideInlay: Point,
fontMetricsStorage: InlayTextMetricsStorage,
)
fun getMouseArea(
pointInsideInlay: Point,
fontMetricsStorage: InlayTextMetricsStorage,
): InlayMouseArea?
}

View File

@@ -1,154 +0,0 @@
// 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.views
import com.intellij.codeInsight.hints.declarative.impl.InlayMouseArea
import com.intellij.codeInsight.hints.presentation.InlayTextMetricsStorage
import com.intellij.formatting.visualLayer.VirtualFormattingInlaysInfo
import com.intellij.openapi.editor.Document
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.Inlay
import com.intellij.openapi.editor.event.EditorMouseEvent
import com.intellij.openapi.editor.ex.util.EditorUtil
import com.intellij.openapi.editor.markup.TextAttributes
import com.intellij.ui.LightweightHint
import com.intellij.util.DocumentUtil
import com.intellij.util.concurrency.annotations.RequiresReadLock
import com.intellij.util.text.CharArrayUtil
import java.awt.Graphics2D
import java.awt.Point
import java.awt.geom.Rectangle2D
/**
* Indents the hint to match the indent of the line given by the offset of the carrying inlay
*
* Caveats:
* - Calculating the width of the indent requires a read action during rendering.
* More correct approach would be to calculate and update it in [com.intellij.codeInsight.hints.declarative.impl.DeclarativeInlayHintsPass],
* however, that creates a noticeable delay between when, e.g., user increases the indent of the line and the time the indent of the hint
* updates.
* - [Inlay.getOffset] will not return the correct value during inlay construction,
* which is when `calcWidthInPixels` will be called for the first time. `initialIndentAnchorOffset` is used as a work-around.
*/
internal class IndentedDeclarativeHintView<View, Model>(val view: View, private val initialIndentAnchorOffset: Int)
: DeclarativeHintView<Model>
where View : DeclarativeHintView<Model> {
lateinit var inlay: Inlay<*>
override fun updateModel(newModel: Model) {
view.updateModel(newModel)
}
private fun getViewIndentMargin(inlay: Inlay<*>? = null): Int =
if (this::inlay.isInitialized) {
calcViewIndentMargin(this.inlay.offset, this.inlay.editor)
}
else if (inlay != null) {
calcViewIndentMargin(initialIndentAnchorOffset, inlay.editor)
}
else {
0
}
override fun calcWidthInPixels(inlay: Inlay<*>, fontMetricsStorage: InlayTextMetricsStorage): Int {
return getViewIndentMargin(inlay) + view.calcWidthInPixels(inlay, fontMetricsStorage)
}
override fun paint(
inlay: Inlay<*>,
g: Graphics2D,
targetRegion: Rectangle2D,
textAttributes: TextAttributes,
fontMetricsStorage: InlayTextMetricsStorage,
) {
view.paint(inlay, g, targetRegion.toViewRectangle(), textAttributes, fontMetricsStorage)
}
override fun handleLeftClick(
e: EditorMouseEvent,
pointInsideInlay: Point,
fontMetricsStorage: InlayTextMetricsStorage,
controlDown: Boolean,
) {
val translated = pointInsideInlay.toPointInsideViewOrNull() ?: return
view.handleLeftClick(e, translated, fontMetricsStorage, controlDown)
}
override fun handleHover(
e: EditorMouseEvent,
pointInsideInlay: Point,
fontMetricsStorage: InlayTextMetricsStorage,
): LightweightHint? {
val translated = pointInsideInlay.toPointInsideViewOrNull() ?: return null
return view.handleHover(e, translated, fontMetricsStorage)
}
override fun handleRightClick(e: EditorMouseEvent, pointInsideInlay: Point, fontMetricsStorage: InlayTextMetricsStorage) {
val translated = pointInsideInlay.toPointInsideViewOrNull() ?: return
return view.handleRightClick(e, translated, fontMetricsStorage)
}
override fun getMouseArea(pointInsideInlay: Point, fontMetricsStorage: InlayTextMetricsStorage): InlayMouseArea? {
val translated = pointInsideInlay.toPointInsideViewOrNull() ?: return null
return view.getMouseArea(translated, fontMetricsStorage)
}
private fun Point.toPointInsideViewOrNull(): Point? {
val indentMargin = getViewIndentMargin()
if (this.x < indentMargin) {
return null
}
return Point(this.x - indentMargin, this.y)
}
private fun Rectangle2D.toViewRectangle(): Rectangle2D {
val indentMargin = getViewIndentMargin()
return Rectangle2D.Double(this.x + indentMargin,
this.y,
this.width - indentMargin,
this.height)
}
companion object {
internal fun calcIndentAnchorOffset(offset: Int, document: Document): Pair<Int, Int> {
val lineStartOffset = DocumentUtil.getLineStartOffset(offset, document)
val textStartOffset = CharArrayUtil.shiftForward(document.immutableCharSequence, lineStartOffset, " \t")
return lineStartOffset to textStartOffset
}
}
}
@RequiresReadLock
private fun calcViewIndentMargin(offset: Int, editor: Editor): Int {
val document = editor.document
val text = document.immutableCharSequence
val (lineStartOffset, textStartOffset) = IndentedDeclarativeHintView.calcIndentAnchorOffset(offset, document)
val indentMargin = if (editor.inlayModel.isInBatchMode) {
// avoid coordinate transformations in batch mode
measureIndentSafely(text, lineStartOffset, textStartOffset, editor)
}
else {
val vfmtRightShift = VirtualFormattingInlaysInfo.measureVirtualFormattingInlineInlays(editor, textStartOffset, textStartOffset)
editor.offsetToXY(textStartOffset, false, false).x + vfmtRightShift
}
return indentMargin
}
private fun measureIndentSafely(text: CharSequence, start: Int, end: Int, editor: Editor): Int {
val spaceWidth = EditorUtil.getPlainSpaceWidth(editor)
val tabSize = EditorUtil.getTabSize(editor)
var columns = 0
var offset = start
while (offset < end) {
val c = text[offset++]
when (c) {
'\t' -> {
columns += (columns / tabSize + 1) * tabSize
}
else -> {
columns += 1
}
}
}
return columns * spaceWidth
}

View File

@@ -0,0 +1,110 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.codeInsight.hints.declarative.impl.views
import org.jetbrains.annotations.ApiStatus
import java.awt.Point
@ApiStatus.Internal
interface InlayElementWithMargins<Context> {
fun computeLeftMargin(context: Context): Int
fun computeRightMargin(context: Context): Int
// content and padding (and not including margin)
fun computeBoxWidth(context: Context): Int
}
@ApiStatus.Internal
fun <Context> InlayElementWithMargins<Context>.computeFullWidth(context: Context): Int =
computeLeftMargin(context) + computeBoxWidth(context) + computeRightMargin(context)
@ApiStatus.Internal
abstract class InlayElementWithMarginsCompositeBase<Context, SubView, SubViewContext>()
: Invalidable
where SubView : InlayElementWithMargins<SubViewContext>,
SubView : Invalidable {
protected abstract fun getSubView(index: Int): SubView
protected abstract val subViewCount: Int
abstract fun computeSubViewContext(context: Context): SubViewContext
private var computedSubViewMetrics: SubViewMetrics? = null
protected inline fun forEachSubViewBounds(
context: Context,
action: (SubView, Int, Int) -> Unit,
) {
val sortedBounds = getSubViewMetrics(context).sortedBounds
for (index in 0..<subViewCount) {
val leftBound = sortedBounds[2 * index]
val rightBound = sortedBounds[2 * index + 1]
action(getSubView(index), leftBound, rightBound)
}
}
override fun invalidate() {
computedSubViewMetrics = null
for (index in 0..<subViewCount) {
getSubView(index).invalidate()
}
}
protected inline fun <R> forSubViewAtPoint(
pointInsideInlay: Point,
context: Context,
action: (SubView, Point) -> R,
) : R? {
val x = pointInsideInlay.x
forEachSubViewBounds(context) { subView, leftBound, rightBound ->
if (x in leftBound..<rightBound) {
return action(subView, Point(x - leftBound, pointInsideInlay.y))
}
}
return null
}
open fun getSubViewMetrics(context: Context): SubViewMetrics {
val metrics = computedSubViewMetrics
if (metrics == null) {
val computed = computeSubViewMetrics(context)
computedSubViewMetrics = computed
return computed
}
return metrics
}
private fun computeSubViewMetrics(
context: Context,
): SubViewMetrics {
val subViewContext = computeSubViewContext(context)
val sortedBounds = IntArray(subViewCount * 2)
var xSoFar = 0
var previousRightMargin = 0
for (index in 0..<subViewCount) {
val subView = getSubView(index)
val leftMargin = subView.computeLeftMargin(subViewContext)
val leftBound = xSoFar + maxOf(previousRightMargin, leftMargin)
val rightBound = leftBound + subView.computeBoxWidth(subViewContext)
sortedBounds[2 * index] = leftBound
sortedBounds[2 * index + 1] = rightBound
previousRightMargin = subView.computeRightMargin(subViewContext)
xSoFar = rightBound
}
return SubViewMetrics(sortedBounds, sortedBounds.last() + previousRightMargin)
}
}
/**
* [sortedBounds] is an array of left- and right-bound offset pairs along the x-axis.
* - The left-margin always starts at 0 offset.
*
* [fullWidth] is the last right-bound + margin of the last element.
*/
@ApiStatus.Internal
class SubViewMetrics(val sortedBounds: IntArray, val fullWidth: Int) {
val leftMargin: Int
get() = sortedBounds.first()
val rightMargin: Int
get() = fullWidth - sortedBounds.last()
val boxWidth: Int
get() = sortedBounds.last() - sortedBounds.first()
}

View File

@@ -0,0 +1,102 @@
// 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.views
import com.intellij.codeInsight.hints.declarative.AboveLineIndentedPosition
import com.intellij.codeInsight.hints.declarative.EndOfLinePosition
import com.intellij.codeInsight.hints.declarative.InlayPosition
import com.intellij.codeInsight.hints.declarative.InlineInlayPosition
import com.intellij.codeInsight.hints.declarative.impl.InlayData
import com.intellij.codeInsight.hints.presentation.InlayTextMetricsStorage
import com.intellij.openapi.editor.Inlay
import com.intellij.openapi.editor.markup.TextAttributes
import java.awt.Graphics2D
import java.awt.Point
import java.awt.Rectangle
import java.awt.geom.Rectangle2D
import java.util.*
internal class InlayPresentationComposite(inlayData: List<InlayData>)
: InlayTopLevelElement<List<InlayData>>,
InlayElementWithMarginsCompositeBase<InlayTextMetricsStorage, InlayPresentationList, InlayTextMetricsStorage>() {
var presentationLists: List<InlayPresentationList> =
if (inlayData.size == 1) {
listOf((InlayPresentationList(inlayData.first())))
}
else {
inlayData.map { InlayPresentationList(it) }
}
private set
override val subViewCount: Int get() = presentationLists.size
override fun getSubView(index: Int): InlayPresentationList = presentationLists[index]
override fun computeSubViewContext(context: InlayTextMetricsStorage): InlayTextMetricsStorage = context
override fun paint(inlay: Inlay<*>, g: Graphics2D, targetRegion: Rectangle2D, textAttributes: TextAttributes, textMetricsStorage: InlayTextMetricsStorage) {
forEachSubViewBounds(textMetricsStorage) { subView, leftBound, rightBound ->
val width = rightBound - leftBound
val currentRegion = Rectangle(targetRegion.x.toInt() + leftBound, targetRegion.y.toInt(), width, targetRegion.height.toInt())
subView.paint(inlay, g, currentRegion, textAttributes, textMetricsStorage)
}
}
override fun findEntryAtPoint(pointInsideInlay: Point, textMetricsStorage: InlayTextMetricsStorage): CapturedPointInfo? {
forSubViewAtPoint(pointInsideInlay, textMetricsStorage) { subView, pointInsideSubView ->
return subView.findEntryAtPoint(pointInsideSubView, textMetricsStorage)
}
return null
}
override fun updateModel(newModel: List<InlayData>) {
/* We have no reliable way to tell if the new hints are the same hints as the ones being updated or not.
It is a trade-off between correctness and efficiency.
Being incorrect means user CollapseState information is not preserved between DeclarativeInlayHintPasses.
(See InlayPresentationList#toggleTreeState) */
if (newModel.size == presentationLists.size) {
// Assume same hints
presentationLists.forEachIndexed { index, presentationList -> presentationList.updateModel(newModel[index]) }
return
}
// Different set of hints from the same provider -- try distinguishing hints using their priorities (assuming those are stable).
var oldIndex = 0
var newIndex = 0
val newPresentationLists = ArrayList<InlayPresentationList>(newModel.size)
while (oldIndex < presentationLists.size && newIndex < newModel.size) {
val oldPrio = presentationLists[oldIndex].model.position.priority
val newPrio = newModel[newIndex].position.priority
when {
oldPrio == newPrio -> {
newPresentationLists.add(presentationLists[oldIndex].also { it.updateModel(newModel[newIndex]) })
oldIndex++
newIndex++
}
oldPrio < newPrio -> {
// assume it's more likely a hint was removed rather than some priority would change
oldIndex++
}
else /* oldPrio > newPrio */ -> {
// assume it's more likely a hint was added
newPresentationLists.add(InlayPresentationList(newModel[newIndex]))
newIndex++
}
}
}
while (newIndex < newModel.size) {
newPresentationLists.add(InlayPresentationList(newModel[newIndex]))
newIndex++
}
presentationLists = newPresentationLists
invalidate()
}
override fun computeLeftMargin(context: InlayTextMetricsStorage): Int = 0
override fun computeRightMargin(context: InlayTextMetricsStorage): Int = 0
override fun computeBoxWidth(context: InlayTextMetricsStorage): Int = getSubViewMetrics(context).fullWidth
}
private val InlayPosition.priority: Int get() = when(this) {
is AboveLineIndentedPosition -> priority
is EndOfLinePosition -> priority
is InlineInlayPosition -> priority
}

View File

@@ -1,15 +1,11 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.codeInsight.hints.declarative.impl.views
import com.intellij.codeInsight.hints.declarative.impl.DeclarativeInlayActionService
import com.intellij.codeInsight.hints.declarative.impl.InlayMouseArea
import com.intellij.codeInsight.hints.presentation.InlayTextMetrics
import com.intellij.ide.ui.AntialiasingType
import com.intellij.openapi.components.service
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.event.EditorMouseEvent
import com.intellij.openapi.editor.markup.TextAttributes
import com.intellij.ui.paint.EffectPainter
import org.jetbrains.annotations.ApiStatus.Internal
import org.jetbrains.annotations.TestOnly
import java.awt.Graphics2D
@@ -19,8 +15,9 @@ import kotlin.math.max
@Internal
sealed class InlayPresentationEntry(
@TestOnly
val clickArea: InlayMouseArea?
) {
val clickArea: InlayMouseArea?,
val parentIndexToSwitch: Byte,
) : InlayElementWithMargins<InlayTextMetrics>, Invalidable {
abstract fun render(
graphics: Graphics2D,
metrics: InlayTextMetrics,
@@ -28,15 +25,9 @@ sealed class InlayPresentationEntry(
isDisabled: Boolean,
yOffset: Int,
rectHeight: Int,
editor: Editor
editor: Editor,
)
abstract fun computeWidth(metrics: InlayTextMetrics): Int
abstract fun computeHeight(metrics: InlayTextMetrics): Int
abstract fun handleClick(e: EditorMouseEvent, list: InlayPresentationList, controlDown: Boolean)
var isHoveredWithCtrl: Boolean = false
}
@@ -44,49 +35,30 @@ sealed class InlayPresentationEntry(
class TextInlayPresentationEntry(
@TestOnly
val text: String,
private val parentIndexToSwitch: Byte = -1,
clickArea: InlayMouseArea?
) : InlayPresentationEntry(clickArea) {
parentIndexToSwitch: Byte = -1,
clickArea: InlayMouseArea?,
) : InlayPresentationEntry(clickArea, parentIndexToSwitch) {
override fun handleClick(e: EditorMouseEvent, list: InlayPresentationList, controlDown: Boolean) {
val editor = e.editor
val project = editor.project
if (clickArea != null && project != null) {
val actionData = clickArea.actionData
if (controlDown) {
service<DeclarativeInlayActionService>().invokeActionHandler(actionData, e)
}
}
if (parentIndexToSwitch != (-1).toByte()) {
list.toggleTreeState(parentIndexToSwitch)
}
}
override fun render(graphics: Graphics2D,
metrics: InlayTextMetrics,
attributes: TextAttributes,
isDisabled: Boolean,
yOffset: Int,
rectHeight: Int,
editor: Editor) {
override fun render(
graphics: Graphics2D,
metrics: InlayTextMetrics,
attributes: TextAttributes,
isDisabled: Boolean,
yOffset: Int,
rectHeight: Int,
editor: Editor,
) {
val savedHint = graphics.getRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING)
val savedColor = graphics.color
try {
val foreground = attributes.foregroundColor
if (foreground != null) {
val width = computeWidth(metrics)
val height = computeHeight(metrics)
val font = metrics.font
graphics.font = font
graphics.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, AntialiasingType.getKeyForCurrentScope(false))
graphics.color = foreground
val baseline = max(editor.ascent, (rectHeight + metrics.ascent - metrics.descent) / 2) - 1
graphics.drawString(text, 0, baseline)
val effectColor = attributes.effectColor ?: foreground
if (isDisabled) {
graphics.color = effectColor
EffectPainter.STRIKE_THROUGH.paint(graphics, 0, baseline + yOffset, width, height, font)
}
}
}
finally {
@@ -95,10 +67,6 @@ class TextInlayPresentationEntry(
}
}
override fun computeWidth(metrics: InlayTextMetrics): Int = metrics.getStringWidth(text)
override fun computeHeight(metrics: InlayTextMetrics): Int = metrics.fontHeight
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
@@ -115,4 +83,11 @@ class TextInlayPresentationEntry(
override fun toString(): String {
return "TextInlayPresentationEntry(text='$text', parentIndexToSwitch=$parentIndexToSwitch)"
}
override fun computeLeftMargin(context: InlayTextMetrics): Int = 0
override fun computeRightMargin(context: InlayTextMetrics): Int = 0
override fun computeBoxWidth(context: InlayTextMetrics): Int = context.getStringWidth(text)
override fun invalidate() {}
}

View File

@@ -4,24 +4,18 @@ package com.intellij.codeInsight.hints.declarative.impl.views
import com.intellij.codeInsight.hints.declarative.HintColorKind
import com.intellij.codeInsight.hints.declarative.HintFontSize
import com.intellij.codeInsight.hints.declarative.HintMarginPadding
import com.intellij.codeInsight.hints.declarative.impl.DeclarativeInlayActionService
import com.intellij.codeInsight.hints.declarative.impl.InlayData
import com.intellij.codeInsight.hints.declarative.impl.InlayMouseArea
import com.intellij.codeInsight.hints.declarative.impl.InlayTags
import com.intellij.codeInsight.hints.declarative.impl.util.TinyTree
import com.intellij.codeInsight.hints.presentation.InlayTextMetrics
import com.intellij.codeInsight.hints.presentation.InlayTextMetricsStorage
import com.intellij.codeInsight.hints.presentation.PresentationFactory
import com.intellij.openapi.components.service
import com.intellij.codeInsight.hints.presentation.scaleByFont
import com.intellij.openapi.editor.DefaultLanguageHighlighterColors
import com.intellij.openapi.editor.Inlay
import com.intellij.openapi.editor.colors.EditorColors
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.ui.paint.EffectPainter
import com.intellij.util.concurrency.annotations.RequiresEdt
import com.intellij.util.containers.enumMapOf
import com.intellij.util.ui.GraphicsUtil
@@ -31,99 +25,36 @@ import org.jetbrains.annotations.TestOnly
import java.awt.Graphics2D
import java.awt.Point
import java.awt.geom.Rectangle2D
import kotlin.math.max
/**
* @see com.intellij.codeInsight.hints.declarative.impl.PresentationTreeBuilderImpl
*/
@ApiStatus.Internal
class InlayPresentationList(
@ApiStatus.Internal var model: InlayData,
private val onStateUpdated: () -> Unit
) : DeclarativeHintView<InlayData>, ViewWithMargins {
model: InlayData,
) : InlayElementWithMarginsCompositeBase<InlayTextMetricsStorage, InlayPresentationEntry, InlayTextMetrics>(),
InlayElementWithMargins<InlayTextMetricsStorage>,
InlayTopLevelElement<InlayData> {
var model: InlayData = model
private set
private var entries: Array<InlayPresentationEntry> = model.tree.buildPresentationEntries()
private var _partialWidthSums: IntArray? = null
private fun TinyTree<Any?>.buildPresentationEntries(): Array<InlayPresentationEntry> {
return PresentationEntryBuilder(this, model.providerClass).buildPresentationEntries()
}
private fun computePartialSums(textMetrics: InlayTextMetrics): IntArray {
var widthSoFar = 0
return IntArray(entries.size) {
val entry = entries[it]
widthSoFar += entry.computeWidth(textMetrics)
widthSoFar
}
}
private fun getPartialWidthSums(storage: InlayTextMetricsStorage, forceRecompute: Boolean = false): IntArray {
val sums = _partialWidthSums
if (sums == null || forceRecompute) {
val metrics = getMetrics(storage)
val computed = computePartialSums(metrics)
_partialWidthSums = computed
return computed
}
return sums
}
override fun handleLeftClick(
e: EditorMouseEvent,
pointInsideInlay: Point,
fontMetricsStorage: InlayTextMetricsStorage,
controlDown: Boolean,
) {
val entry = findEntryByPoint(fontMetricsStorage, pointInsideInlay) ?: return
SlowOperations.startSection(SlowOperations.ACTION_PERFORM).use {
entry.handleClick(e, this, controlDown)
}
}
override fun handleRightClick(
e: EditorMouseEvent,
pointInsideInlay: Point,
fontMetricsStorage: InlayTextMetricsStorage,
) {
service<DeclarativeInlayActionService>().invokeInlayMenu(model, e, RelativePoint(e.mouseEvent.locationOnScreen))
}
private val marginAndPadding: Pair<Int, Int> get() = MARGIN_PADDING_BY_FORMAT[model.hintFormat.horizontalMarginPadding]!!
@get:ApiStatus.Internal
override val margin: Int get() = marginAndPadding.first
private val padding: Int get() = marginAndPadding.second
private fun getTextWidth(storage: InlayTextMetricsStorage, forceUpdate: Boolean): Int {
return getPartialWidthSums(storage, forceUpdate).lastOrNull() ?: 0
}
@ApiStatus.Internal
override fun getBoxWidth(storage: InlayTextMetricsStorage, forceUpdate: Boolean): Int {
return 2 * padding + getTextWidth(storage, forceUpdate)
}
private fun findEntryByPoint(fontMetricsStorage: InlayTextMetricsStorage, pointInsideInlay: Point): InlayPresentationEntry? {
val partialWidthSums = getPartialWidthSums(fontMetricsStorage)
val initialLeftBound = padding
val x = pointInsideInlay.x - initialLeftBound
var previousWidthSum = 0
for ((index, entry) in entries.withIndex()) {
val leftBound = previousWidthSum
val rightBound = partialWidthSums[index]
override fun computeLeftMargin(context: InlayTextMetricsStorage): Int = marginAndPadding.first
if (x in leftBound..<rightBound) {
return entry
}
previousWidthSum = partialWidthSums[index]
}
return null
}
override fun computeRightMargin(context: InlayTextMetricsStorage): Int = marginAndPadding.first
override fun handleHover(
e: EditorMouseEvent,
pointInsideInlay: Point,
fontMetricsStorage: InlayTextMetricsStorage,
): LightweightHint? {
val tooltip = model.tooltip
return if (tooltip == null) null
else PresentationFactory(e.editor).showTooltip(e.mouseEvent, tooltip)
private fun getPadding(context: InlayTextMetricsStorage): Int = marginAndPadding.second
override fun computeBoxWidth(context: InlayTextMetricsStorage): Int {
val entriesMetrics = getSubViewMetrics(context)
return max(getPadding(context), entriesMetrics.leftMargin) +
entriesMetrics.boxWidth +
max(getPadding(context), entriesMetrics.rightMargin)
}
@RequiresEdt
@@ -131,8 +62,7 @@ class InlayPresentationList(
updateStateTree(newModel.tree, this.model.tree, 0, 0)
this.model = newModel
this.entries = newModel.tree.buildPresentationEntries()
this._partialWidthSums = null
onStateUpdated()
invalidate()
}
private fun updateStateTree(
@@ -161,6 +91,13 @@ class InlayPresentationList(
}
}
override fun findEntryAtPoint(pointInsideInlay: Point, textMetricsStorage: InlayTextMetricsStorage): CapturedPointInfo {
val entry = forSubViewAtPoint(pointInsideInlay, textMetricsStorage) { entry, _ ->
entry
}
return CapturedPointInfo(this, entry)
}
internal fun toggleTreeState(parentIndexToSwitch: Byte) {
val tree = model.tree
when (val payload = tree.getBytePayload(parentIndexToSwitch)) {
@@ -177,20 +114,18 @@ class InlayPresentationList(
updateModel(this.model)
}
@ApiStatus.Internal
override fun calcWidthInPixels(inlay: Inlay<*>, textMetricsStorage: InlayTextMetricsStorage): Int = getBoxWidth(textMetricsStorage)
override fun paint(
inlay: Inlay<*>,
g: Graphics2D,
targetRegion: Rectangle2D,
textAttributes: TextAttributes,
storage: InlayTextMetricsStorage,
textMetricsStorage: InlayTextMetricsStorage,
) {
val editor = inlay.editor as EditorImpl
var xOffset = 0
val metrics = getMetrics(storage)
val gap = if (targetRegion.height.toInt() < metrics.lineHeight + 2) 1 else 2
val rectHeight = targetRegion.height.toInt()
val rectWidth = targetRegion.width.toInt()
val metrics = getMetrics(textMetricsStorage)
val gap = if (rectHeight < metrics.lineHeight + 2) 1 else 2
val hintFormat = model.hintFormat
val attrKey = when (hintFormat.colorKind) {
HintColorKind.Default -> DefaultLanguageHighlighterColors.INLAY_DEFAULT
@@ -198,21 +133,20 @@ class InlayPresentationList(
HintColorKind.TextWithoutBackground -> DefaultLanguageHighlighterColors.INLAY_TEXT_WITHOUT_BACKGROUND
}
val attrs = editor.colorsScheme.getAttributes(attrKey)
g.withTranslated(targetRegion.x, targetRegion.y) {
if (hintFormat.colorKind.hasBackground()) {
val rectHeight = targetRegion.height.toInt() - gap * 2
val rectHeight = rectHeight - gap * 2
val config = GraphicsUtil.setupAAPainting(g)
GraphicsUtil.paintWithAlpha(g, BACKGROUND_ALPHA)
g.color = attrs.backgroundColor ?: textAttributes.backgroundColor
g.fillRoundRect(0, gap, getBoxWidth(storage), rectHeight, ARC_WIDTH, ARC_HEIGHT)
g.fillRoundRect(0, gap, computeBoxWidth(textMetricsStorage), rectHeight, scaleByFont(ARC_WIDTH, metrics.font.size2D), scaleByFont(ARC_HEIGHT, metrics.font.size2D))
config.restore()
}
}
g.withTranslated(padding + targetRegion.x, targetRegion.y) {
val partialWidthSums = getPartialWidthSums(storage)
for ((index, entry) in entries.withIndex()) {
g.withTranslated(getPadding(textMetricsStorage) + targetRegion.x, targetRegion.y) {
forEachSubViewBounds(textMetricsStorage) { entry, leftBound, _ ->
val hoveredWithCtrl = entry.isHoveredWithCtrl
val finalAttrs = if (hoveredWithCtrl) {
val refAttrs = inlay.editor.colorsScheme.getAttributes(EditorColors.REFERENCE_HYPERLINK_COLOR)
@@ -223,10 +157,20 @@ class InlayPresentationList(
else {
attrs
}
g.withTranslated(xOffset, 0) {
g.withTranslated(leftBound, 0) {
entry.render(g, metrics, finalAttrs, model.disabled, gap, targetRegion.height.toInt(), editor)
}
xOffset = partialWidthSums[index]
}
if (model.disabled) {
val savedColor = g.color
try {
val effectColor = textAttributes.effectColor ?: textAttributes.foregroundColor ?: return@withTranslated
g.color = effectColor
EffectPainter.STRIKE_THROUGH.paint(g, 0, editor.ascent, rectWidth - 2 * getPadding(textMetricsStorage), metrics.ascent, metrics.font)
}
finally {
g.color = savedColor
}
}
}
}
@@ -239,10 +183,12 @@ class InlayPresentationList(
return entries
}
override fun getMouseArea(pointInsideInlay: Point, fontMetricsStorage: InlayTextMetricsStorage): InlayMouseArea? {
val entry = findEntryByPoint(fontMetricsStorage, pointInsideInlay) ?: return null
return entry.clickArea
}
override fun getSubView(index: Int): InlayPresentationEntry = entries[index]
override val subViewCount: Int
get() = entries.size
override fun computeSubViewContext(context: InlayTextMetricsStorage): InlayTextMetrics = getMetrics(context)
}
private val MARGIN_PADDING_BY_FORMAT = enumMapOf<HintMarginPadding, Pair<Int, Int>>().apply {

View File

@@ -0,0 +1,51 @@
// 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.views
import com.intellij.codeInsight.hints.presentation.InlayTextMetricsStorage
import com.intellij.openapi.editor.Inlay
import com.intellij.openapi.editor.markup.TextAttributes
import com.intellij.util.concurrency.annotations.RequiresEdt
import org.jetbrains.annotations.ApiStatus
import java.awt.Graphics2D
import java.awt.Point
import java.awt.geom.Rectangle2D
/**
* There are 2-3 levels to declarative inlay presentation:
* 1. [InlayPresentationEntry] represents a leaf of the [inlay tree][com.intellij.codeInsight.hints.declarative.impl.InlayData.tree].
* One entry usually corresponds to a [PresentationTreeBuilder.text][com.intellij.codeInsight.hints.declarative.PresentationTreeBuilder.text]
* or [icon][com.intellij.codeInsight.hints.declarative.PresentationTreeBuilder.icon] call.
* 2. [InlayPresentationList] represents a subsequence of the leaves of an [inlay tree][com.intellij.codeInsight.hints.declarative.impl.InlayData.tree].
* If there are no [collapsibleList][com.intellij.codeInsight.hints.declarative.PresentationTreeBuilder.collapsibleList] branches in the tree,
* then it is a sequence of all the leaves.
* Corresponds to a single [com.intellij.codeInsight.hints.declarative.InlayTreeSink.addPresentation] call.
* 3. Optional. [InlayPresentationComposite] is used when multiple declarative inlays need to be placed in a single editor inlay,
* e.g.; multiple [above-line][com.intellij.codeInsight.hints.declarative.AboveLineIndentedPosition] inlays rendered on a single
* [block inlay element][com.intellij.openapi.editor.InlayModel.addBlockElement]
*/
@ApiStatus.Internal
interface InlayTopLevelElement<Model> : Invalidable, InlayElementWithMargins<InlayTextMetricsStorage> {
@RequiresEdt
fun paint(
inlay: Inlay<*>,
g: Graphics2D,
targetRegion: Rectangle2D,
textAttributes: TextAttributes,
textMetricsStorage: InlayTextMetricsStorage,
)
@RequiresEdt
fun findEntryAtPoint(pointInsideInlay: Point, textMetricsStorage: InlayTextMetricsStorage): CapturedPointInfo?
@RequiresEdt
fun updateModel(newModel: Model)
}
@ApiStatus.Internal
interface Invalidable {
@RequiresEdt
fun invalidate()
}
@ApiStatus.Internal
data class CapturedPointInfo(val presentationList: InlayPresentationList, val entry: InlayPresentationEntry?)

View File

@@ -1,105 +0,0 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.codeInsight.hints.declarative.impl.views
import com.intellij.codeInsight.hints.presentation.InlayTextMetricsStamp
import com.intellij.codeInsight.hints.presentation.InlayTextMetricsStorage
import org.jetbrains.annotations.ApiStatus
import java.awt.Point
@ApiStatus.Internal
interface ViewWithMargins {
val margin: Int
fun getBoxWidth(storage: InlayTextMetricsStorage, forceUpdate: Boolean = false): Int
}
@ApiStatus.Internal
abstract class ViewWithMarginsCompositeBase<SubView>(private val ignoreInitialMargin: Boolean)
where SubView : ViewWithMargins {
protected abstract fun getSubView(index: Int): SubView
protected abstract val subViewCount: Int
private var computedSubViewMetrics: SubViewMetrics? = null
private var inlayTextMetricsStamp: InlayTextMetricsStamp? = null
protected inline fun forEachSubViewBounds(
fontMetricsStorage: InlayTextMetricsStorage,
action: (SubView, Int, Int) -> Unit,
) {
val sortedBounds = getSubViewMetrics(fontMetricsStorage).sortedBounds
for (index in 0..<subViewCount) {
val leftBound = sortedBounds[2 * index]
val rightBound = sortedBounds[2 * index + 1]
action(getSubView(index), leftBound, rightBound)
}
}
internal fun invalidateComputedSubViewMetrics() {
computedSubViewMetrics = null
}
protected inline fun forSubViewAtPoint(
pointInsideInlay: Point,
fontMetricsStorage: InlayTextMetricsStorage,
action: (SubView, Point) -> Unit,
) {
val x = pointInsideInlay.x
forEachSubViewBounds(fontMetricsStorage) { subView, leftBound, rightBound ->
if (x in leftBound..<rightBound) {
action(subView, Point(x - leftBound, pointInsideInlay.y))
return
}
}
}
protected fun getSubViewMetrics(fontMetricsStorage: InlayTextMetricsStorage): SubViewMetrics {
val metrics = computedSubViewMetrics
val currentStamp = getCurrentTextMetricsStamp(fontMetricsStorage)
val areFontMetricsActual = areFontMetricsActual(currentStamp)
if (metrics == null || !areFontMetricsActual) {
val computed = computeSubViewMetrics(ignoreInitialMargin, fontMetricsStorage, !areFontMetricsActual)
computedSubViewMetrics = computed
inlayTextMetricsStamp = currentStamp
return computed
}
return metrics
}
private fun computeSubViewMetrics(
ignoreInitialMargin: Boolean,
fontMetricsStorage: InlayTextMetricsStorage,
forceUpdate: Boolean
): SubViewMetrics {
val sortedBounds = IntArray(subViewCount * 2)
var xSoFar = 0
var previousMargin = 0
getSubView(0).let { subView ->
val margin = subView.margin
sortedBounds[0] = if (ignoreInitialMargin) 0 else subView.margin
sortedBounds[1] = sortedBounds[0] + subView.getBoxWidth(fontMetricsStorage, forceUpdate)
previousMargin = margin
}
xSoFar = sortedBounds[1]
for (index in 1..<subViewCount) {
val subView = getSubView(index)
val margin = subView.margin
val leftBound = xSoFar + maxOf(previousMargin, margin)
val rightBound = leftBound + subView.getBoxWidth(fontMetricsStorage, forceUpdate)
sortedBounds[2 * index] = leftBound
sortedBounds[2 * index + 1] = rightBound
previousMargin = margin
xSoFar = rightBound
}
return SubViewMetrics(sortedBounds, sortedBounds.last() + previousMargin)
}
protected open fun getCurrentTextMetricsStamp(fontMetricsStorage: InlayTextMetricsStorage): InlayTextMetricsStamp? {
return fontMetricsStorage.getCurrentStamp()
}
protected open fun areFontMetricsActual(currentStamp: InlayTextMetricsStamp?): Boolean {
return inlayTextMetricsStamp == currentStamp
}
}
@ApiStatus.Internal
class SubViewMetrics(val sortedBounds: IntArray, val fullWidth: Int)

View File

@@ -114,9 +114,9 @@ class InlayTextMetrics(
editor: Editor,
val fontHeight: Int,
val fontBaseline: Int,
private val fontMetrics: FontMetrics,
val fontMetrics: FontMetrics,
val fontType: Int,
private val ideScale: Float,
val ideScale: Float,
) {
companion object {
internal fun create(editor: Editor, size: Float, fontType: Int, context: FontRenderContext) : InlayTextMetrics {
@@ -142,6 +142,7 @@ class InlayTextMetrics(
val ascent: Int = editor.ascent
val descent: Int = (editor as? EditorImpl)?.descent ?: 0
val lineHeight: Int = editor.lineHeight
val spaceWidth: Int = EditorUtil.getPlainSpaceWidth(editor)
private val editorComponent = editor.component
@Deprecated("Use InlayTextMetricsStorage.getCurrentStamp() to ensure actual metrics are used")

View File

@@ -1,94 +0,0 @@
// 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.views.CompositeDeclarativeHintWithMarginsView
import com.intellij.codeInsight.hints.declarative.impl.views.DeclarativeHintView
import com.intellij.codeInsight.hints.declarative.impl.views.ViewWithMargins
import com.intellij.codeInsight.hints.presentation.InlayTextMetricsStamp
import com.intellij.codeInsight.hints.presentation.InlayTextMetricsStorage
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.event.EditorMouseEvent
import org.jmock.Mockery
import org.jmock.lib.legacy.ClassImposteriser
import org.junit.jupiter.api.*
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertTrue
import java.awt.Point
private val context = Mockery().apply {
setImposteriser(ClassImposteriser.INSTANCE)
}
private val mockEditor = context.mock(Editor::class.java)
private val mockFontMetricsStorage = InlayTextMetricsStorage(mockEditor)
private val mockEditorMouseEvent = context.mock(EditorMouseEvent::class.java)
private val mockPresentationLists = listOf(
5 to 10,
10 to 15,
15 to 20
).map { (margin, boxWidth) -> MockPresentationList(margin, boxWidth) }
private val compositeView = TestCompositeView(mockPresentationLists)
private fun TestCompositeView.simulateRightClick(x: Int, y: Int) {
handleRightClick(mockEditorMouseEvent, Point(x, y), mockFontMetricsStorage)
}
class CompositeDeclarativeHintWithMarginsViewTest {
@BeforeEach
fun resetPresentationLists() {
mockPresentationLists.forEach { it.clicked = false }
}
@Test
fun `click is correctly propagated`() {
compositeView.simulateRightClick(25, 0)
assertEquals(listOf(false, true, false), mockPresentationLists.map { it.clicked })
}
@Test
fun `clicks around right edge`() {
compositeView.simulateRightClick(75, 0)
assertEquals(listOf(false, false, false), mockPresentationLists.map { it.clicked })
assertTrue(mockPresentationLists.none { it.clicked })
compositeView.simulateRightClick(74, 0)
assertEquals(listOf(false, false, true), mockPresentationLists.map { it.clicked })
}
@Test
fun `click inside margin is ignored`() {
compositeView.simulateRightClick(20, 0)
assertTrue(mockPresentationLists.none { it.clicked })
}
}
private class TestCompositeView(val subViews: List<MockPresentationList>)
: CompositeDeclarativeHintWithMarginsView<List<InlayData>, MockPresentationList, InlayData>(false) {
override fun getSubView(index: Int): MockPresentationList = subViews[index]
override val subViewCount: Int = subViews.size
override fun getCurrentTextMetricsStamp(fontMetricsStorage: InlayTextMetricsStorage): InlayTextMetricsStamp? {
return null
}
override fun areFontMetricsActual(currentStamp: InlayTextMetricsStamp?): Boolean {
return false
}
override fun updateModel(newModel: List<InlayData>) = fail("Should not be called")
}
interface DeclarativeHintViewWithMargins : DeclarativeHintView<InlayData>, ViewWithMargins
private val mockDeclarativeHintViewWithMargins = context.mock(DeclarativeHintViewWithMargins::class.java)
private class MockPresentationList(override val margin: Int, val boxWidth: Int)
: DeclarativeHintViewWithMargins by mockDeclarativeHintViewWithMargins {
var clicked = false
override fun getBoxWidth(storage: InlayTextMetricsStorage, forceUpdate: Boolean): Int = boxWidth
override fun handleRightClick(e: EditorMouseEvent, pointInsideInlay: Point, fontMetricsStorage: InlayTextMetricsStorage) {
clicked = true
}
}

View File

@@ -1,14 +1,25 @@
// 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.codeInsight.hints.declarative.impl.views.CapturedPointInfo
import com.intellij.codeInsight.hints.declarative.impl.views.InlayPresentationEntry
import com.intellij.codeInsight.hints.declarative.impl.views.InlayPresentationList
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.Inlay
import com.intellij.openapi.editor.event.EditorMouseEvent
import java.awt.event.MouseEvent
fun InlayPresentationEntry.simulateClick(editor: Editor, presentationList: InlayPresentationList) {
this.handleClick(dummyEditorMouseEvent(editor), presentationList, true)
fun InlayPresentationEntry.simulateClick(
inlay: Inlay<out DeclarativeInlayRendererBase<*>>,
presentationList: InlayPresentationList,
) {
inlay.renderer.getInteractionHandler().handleLeftClick(
inlay,
CapturedPointInfo(presentationList, this),
dummyEditorMouseEvent(inlay.editor),
false,
)
}
private fun dummyEditorMouseEvent(editor: Editor): EditorMouseEvent {

View File

@@ -119,7 +119,12 @@ class DeclarativeInlayMultipleHintsPassTest : DeclarativeInlayHintPassTestBase()
val inlays1 = getBlockInlays()
assertEquals(listOf("one|two"), inlays1.map { it.toText() })
// expand hints
inlays1.flatMap { it.renderer.presentationLists }.forEach { it.getEntries().single().simulateClick(myFixture.editor, it) }
inlays1
.zip(inlays1.map { it.renderer.presentationLists })
.flatMap { (inlay, lists) -> lists.map { inlay to it } }
.forEach { (inlay, list) ->
list.getEntries().single().simulateClick(inlay, list)
}
assertEquals(listOf("1|2"), inlays1.map { it.toText() })
provider.hintAdder = adder
@@ -160,7 +165,13 @@ class DeclarativeInlayMultipleHintsPassTest : DeclarativeInlayHintPassTestBase()
val inlays1 = getBlockInlays()
assertEquals(listOf("two|one"), inlays1.map { it.toText() })
// expand hints
inlays1.flatMap { it.renderer.presentationLists }.first().let { it.getEntries().single().simulateClick(myFixture.editor, it) }
inlays1
.zip(inlays1.map { it.renderer.presentationLists })
.flatMap { (inlay, lists) -> lists.map { inlay to it } }
.first()
.let { (inlay, list) ->
list.getEntries().single().simulateClick(inlay, list)
}
assertEquals(listOf("2|one"), inlays1.map { it.toText() })
text2 = null

View File

@@ -0,0 +1,78 @@
// 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.views.InlayElementWithMargins
import com.intellij.codeInsight.hints.declarative.impl.views.InlayElementWithMarginsCompositeBase
import com.intellij.codeInsight.hints.declarative.impl.views.Invalidable
import com.intellij.codeInsight.hints.declarative.impl.views.SubViewMetrics
import org.junit.jupiter.api.Assertions.assertArrayEquals
import org.junit.jupiter.api.Assertions.assertSame
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertNull
import java.awt.Point
private val mockElems = listOf(
Triple(5, 10, 3),
Triple(10,15, 8),
Triple(6,20, 13)
).map { (leftMargin, boxWidth, rightMargin) ->
object : MockInlayElementWithMargins {
override fun computeLeftMargin(context: Unit): Int = leftMargin
override fun computeRightMargin(context: Unit): Int = rightMargin
override fun computeBoxWidth(context: Unit): Int = boxWidth
override fun invalidate() {}
}
}
private val compositeView = TestCompositeView(mockElems)
private fun TestCompositeView.capturePoint(x: Int, y: Int): MockInlayElementWithMargins? {
return forSubViewAtPoint(Point(x, y)) { elem, _ -> elem }
}
class InlayElementWithMarginsCompositeTest {
@Test
fun `point is correctly captured`() {
val elem = compositeView.capturePoint(25, 0)
assertSame(mockElems[1], elem)
}
@Test
fun `computed metrics are correct`() {
val metrics = compositeView.getSubViewMetrics(Unit)
val expected = SubViewMetrics(
intArrayOf(5, 15, 25, 40, 48, 68),
81
)
assertArrayEquals(expected.sortedBounds, metrics.sortedBounds)
}
@Test
fun `points around right edge`() {
assertNull(compositeView.capturePoint(69, 0))
assertSame(mockElems[2], compositeView.capturePoint(67, 0))
}
@Test
fun `point inside margin is ignored`() {
assertNull(compositeView.capturePoint(20, 0))
}
}
private class TestCompositeView(val subViews: List<MockInlayElementWithMargins>)
: InlayElementWithMarginsCompositeBase<Unit, MockInlayElementWithMargins, Unit>() {
override fun getSubView(index: Int): MockInlayElementWithMargins = subViews[index]
override val subViewCount: Int = subViews.size
override fun computeSubViewContext(context: Unit) = Unit
// so that super.forSubViewAtPoint can remain inline
fun <R> forSubViewAtPoint(
pointInsideInlay: Point,
action: (MockInlayElementWithMargins, Point) -> R
): R? = super.forSubViewAtPoint(pointInsideInlay, Unit, action)
}
interface MockInlayElementWithMargins : InlayElementWithMargins<Unit>, Invalidable

View File

@@ -4,15 +4,11 @@ package com.intellij.codeInsight.hints.declarative.impl
import com.intellij.codeInsight.hints.declarative.*
import com.intellij.codeInsight.hints.declarative.impl.util.TinyTree
import com.intellij.codeInsight.hints.declarative.impl.views.InlayPresentationEntry
import com.intellij.codeInsight.hints.declarative.impl.views.InlayPresentationList
import com.intellij.codeInsight.hints.declarative.impl.views.TextInlayPresentationEntry
import com.intellij.openapi.editor.event.EditorMouseEvent
import com.intellij.testFramework.fixtures.LightPlatformCodeInsightFixture4TestCase
import junit.framework.TestCase
import org.junit.Test
import java.awt.event.MouseEvent
class MouseHandlingEntryTestCase : LightPlatformCodeInsightFixture4TestCase() {
class MouseHandlingEntryTestCase : DeclarativeInlayHintPassTestBase() {
@Test
fun testCollapseExpand1() {
testClick("collapse", "expand", "collapse") {
@@ -175,37 +171,48 @@ class MouseHandlingEntryTestCase : LightPlatformCodeInsightFixture4TestCase() {
afterUpdateText: String
) {
myFixture.configureByText("test.txt", "my text")
val state = buildState {
initialStateBuilder()
val provider = StoredHintsProvider()
val pos = InlineInlayPosition(0, false)
provider.hintAdder = {
addPresentation(pos,
hintFormat = HintFormat.default) {
initialStateBuilder()
}
}
var stateUpdateCallbackInvoked = false
val presentationList = InlayPresentationList(
createInlayData(state, HintFormat.default),
onStateUpdated = {
stateUpdateCallbackInvoked = true
})
val beforeClickEntries = presentationList.getEntries().toList()
assertEquals(beforeClickText, toText(beforeClickEntries))
val editor = myFixture.editor
val providerPassInfo = InlayProviderPassInfo(provider, "test.inlay.provider", emptyMap())
collectAndApplyPass(createPass(providerPassInfo))
val inlay = getInlineInlays().single()
val presentationList = inlay.renderer.presentationList
val metricsBefore = presentationList.getSubViewMetrics(inlay.renderer.textMetricsStorage)
val beforeClickEntries = inlay.renderer.presentationList.getEntries().toList()
assertEquals(beforeClickText, beforeClickEntries.toText())
var occurence = 0
for (beforeClickEntry in beforeClickEntries) {
if ((beforeClickEntry as TextInlayPresentationEntry).text == clickPlace) {
if (occurence == occurenceIndex) {
beforeClickEntry.simulateClick(editor, presentationList)
beforeClickEntry.simulateClick(inlay, presentationList)
break
}
occurence++
}
}
val afterClickEntries = presentationList.getEntries().toList()
assertEquals(afterClickText, toText(afterClickEntries))
assertTrue(stateUpdateCallbackInvoked)
val newState = buildState {
updatedStateBuilder()
assertEquals(afterClickText, afterClickEntries.toText())
val metricsAfterClick = presentationList.getSubViewMetrics(inlay.renderer.textMetricsStorage)
assertNotSame("Inlay presentation was invalidated after collapse click", metricsBefore, metricsAfterClick)
provider.hintAdder = {
addPresentation(pos,
hintFormat = HintFormat.default) {
updatedStateBuilder()
}
}
presentationList.updateModel(createInlayData(newState, HintFormat.default.withColorKind(HintColorKind.TextWithoutBackground)))
collectAndApplyPass(createPass(providerPassInfo))
val updatedStateEntries = presentationList.getEntries().toList()
assertEquals(afterUpdateText, toText(updatedStateEntries))
assertEquals(afterUpdateText, updatedStateEntries.toText())
val metricsAfterUpdate = presentationList.getSubViewMetrics(inlay.renderer.textMetricsStorage)
assertNotSame("Inlay presentation was invalidated after content update", metricsAfterClick, metricsAfterUpdate)
}
private fun PresentationTreeBuilder.genericList(state: CollapseState,
@@ -259,39 +266,30 @@ class MouseHandlingEntryTestCase : LightPlatformCodeInsightFixture4TestCase() {
private fun testClick(beforeClick: String, afterClick: String, click: String, b: PresentationTreeBuilder.() -> Unit) {
myFixture.configureByText("test.txt", "my text")
val root = PresentationTreeBuilderImpl.createRoot()
b(root)
var stateUpdateCallbackInvoked = false
val presentationList = InlayPresentationList(
createInlayData(root.complete()),
onStateUpdated = {
stateUpdateCallbackInvoked = true
val provider = StoredHintsProvider()
val pos = InlineInlayPosition(0, false)
provider.hintAdder = {
addPresentation(pos,
hintFormat = HintFormat.default) {
b()
}
)
}
val providerPassInfo = InlayProviderPassInfo(provider, "test.inlay.provider", emptyMap())
collectAndApplyPass(createPass(providerPassInfo))
val inlay = getInlineInlays().single()
val presentationList = inlay.renderer.presentationList
val metricsBefore = presentationList.getSubViewMetrics(inlay.renderer.textMetricsStorage)
val beforeClickEntries = presentationList.getEntries().toList()
TestCase.assertEquals(beforeClick, toText(beforeClickEntries))
TestCase.assertEquals(beforeClick, beforeClickEntries.toText())
val entry = beforeClickEntries.find { (it as TextInlayPresentationEntry).text == click }!!
val editor = myFixture.editor
val event = MouseEvent(editor.getContentComponent(), 0, 0, 0, 0, 0, 0, false, 0)
entry.handleClick(EditorMouseEvent(editor, event, editor.getMouseEventArea(event)), presentationList, true)
entry.simulateClick(inlay, presentationList)
val afterClickEntries = presentationList.getEntries().toList()
TestCase.assertEquals(afterClick, toText(afterClickEntries))
assertTrue(stateUpdateCallbackInvoked)
TestCase.assertEquals(afterClick, afterClickEntries.toText())
val metricsAfter = presentationList.getSubViewMetrics(inlay.renderer.textMetricsStorage)
assertNotSame("Inlay presentation was invalidated after collapse click", metricsBefore, metricsAfter)
}
}
private fun toText(entries: List<InlayPresentationEntry>): String {
return entries.joinToString(separator = "|") { (it as TextInlayPresentationEntry).text }
}
private fun createInlayData(tree: TinyTree<Any?>, hintFormat: HintFormat = HintFormat.default): InlayData {
return InlayData(InlineInlayPosition(0, true),
null,
hintFormat,
tree,
"dummyProvider",
false,
null,
javaClass,
DeclarativeInlayHintsPass.passSourceId)
}
private fun List<InlayPresentationEntry>.toText(): String {
return joinToString(separator = "|") { (it as TextInlayPresentationEntry).text }
}