mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-04-20 13:31:28 +07:00
[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:
committed by
intellij-monorepo-bot
parent
897108f068
commit
cfb2e2fe61
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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?
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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() {}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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?)
|
||||
@@ -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)
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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 }
|
||||
}
|
||||
Reference in New Issue
Block a user