Inlay hints: introduce updateable roots

GitOrigin-RevId: 7dc566968b76b72fa65d98675da3d830a281a241
This commit is contained in:
Roman.Ivanov
2020-02-18 17:37:27 +07:00
committed by intellij-monorepo-bot
parent f25202e0b1
commit 53ab9acc89
41 changed files with 1827 additions and 542 deletions

View File

@@ -283,6 +283,7 @@ public class JavaLensProvider implements InlayHintsProvider<JavaLensSettings>, E
private static boolean isHoverOverJavaLens(@NotNull Editor editor, @NotNull Point point) {
InlayModel inlayModel = editor.getInlayModel();
Inlay at = inlayModel.getElementAt(point);
return at != null && InlayHintsSinkImpl.Companion.getSettingsKey(at) == KEY;
//return at != null && InlayHintsPass.getSettingsKey(at) == KEY;
return false; // TODO get back!
}
}

View File

@@ -15,14 +15,14 @@
*/
package com.intellij.java.codeInsight.daemon.inlays
import com.intellij.codeInsight.hints.presentation.PresentationRenderer
import com.intellij.codeInsight.hints.LinearOrderInlayRenderer
import com.intellij.testFramework.fixtures.LightJavaCodeInsightFixtureTestCase
import org.intellij.lang.annotations.Language
class MethodChainHintsTest: LightJavaCodeInsightFixtureTestCase() {
fun check(@Language("Java") text: String) {
myFixture.configureByText("A.java", text)
myFixture.testInlays({ (it.renderer as PresentationRenderer).presentation.toString() }, { it.renderer is PresentationRenderer })
myFixture.testInlays({ (it.renderer as LinearOrderInlayRenderer<*>).toString() }, { it.renderer is LinearOrderInlayRenderer<*> })
}
fun `test plain builder`() {

View File

@@ -0,0 +1,45 @@
// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.codeInsight.hints
import com.intellij.codeInsight.hints.presentation.RootInlayPresentation
/**
* Presentation with constraints to the place where it should be placed
*/
interface ConstrainedPresentation<Content : Any, Constraints: Any> {
val root: RootInlayPresentation<Content>
/**
* priority in list during update
*/
val priority: Int
val constraints: Constraints?
}
class HorizontalConstraints(
val priority: Int,
val relatesToPrecedingText: Boolean // specific to placement, but not actually possible to handle in case of multiple hints
)
data class HorizontalConstrainedPresentation<Content : Any>(
override val root: RootInlayPresentation<Content>,
override val constraints: HorizontalConstraints?
) : ConstrainedPresentation<Content, HorizontalConstraints> {
override val priority: Int
get() = constraints?.priority ?: 0
}
class BlockConstraints(
val relatesToPrecedingText: Boolean,
val priority: Int
)
data class BlockConstrainedPresentation<T : Any>(
override val root: RootInlayPresentation<T>,
override val constraints: BlockConstraints?
) : ConstrainedPresentation<T, BlockConstraints>{
override val priority: Int
get() = constraints?.priority ?: 0
}

View File

@@ -9,7 +9,6 @@ import com.intellij.openapi.extensions.ExtensionPointName
import com.intellij.openapi.options.UnnamedConfigurable
import com.intellij.psi.PsiFile
import com.intellij.util.xmlb.annotations.Property
import org.jetbrains.annotations.ApiStatus
import org.jetbrains.annotations.Nls
import javax.swing.JComponent
import kotlin.reflect.KMutableProperty0
@@ -44,7 +43,6 @@ object InlayHintsProviderExtension : LanguageExtension<InlayHintsProvider<*>>(EX
*
* To test it you may use InlayHintsProviderTestCase.
*/
@ApiStatus.Experimental
interface InlayHintsProvider<T : Any> {
/**
* If this method is called, provider is enabled for this file
@@ -163,8 +161,22 @@ class NoSettings {
/**
* Similar to [com.intellij.openapi.util.Key], but it also requires language to be unique
* Allows type-safe access to settings of provider
*/
@Suppress("unused")
data class SettingsKey<T>(val id: String) {
fun getFullId(language: Language): String = language.id + "." + id
}
}
interface AbstractSettingsKey<T: Any> {
fun getFullId(language: Language): String
}
data class InlayKey<T: Any, C: Any>(val id: String) : AbstractSettingsKey<T>, ContentKey<C> {
override fun getFullId(language: Language): String = language.id + "." + id
}
/**
* Allows type-safe access to content of the root presentation
*/
interface ContentKey<C: Any>

View File

@@ -2,11 +2,12 @@
package com.intellij.codeInsight.hints
import com.intellij.codeInsight.hints.presentation.InlayPresentation
import com.intellij.codeInsight.hints.presentation.RootInlayPresentation
import org.jetbrains.annotations.ApiStatus
interface InlayHintsSink {
/**
* Adds inline element to underlying editor.
* Note, that single provider may add only one presentation to the given offset. This requirement may be relaxed in future.
* @see [com.intellij.openapi.editor.InlayModel.addInlineElement]
*/
fun addInlineElement(offset: Int, relatesToPrecedingText: Boolean, presentation: InlayPresentation)
@@ -15,8 +16,19 @@ interface InlayHintsSink {
* Adds block element to underlying editor.
* Offset doesn't affects position of the inlay in the line, it will be drawn in the very beginning of the line.
* Presentation must shift itself (see com.intellij.openapi.editor.ex.util.EditorUtil#getPlainSpaceWidth)
* Note, that single provider may add only one presentation to the given offset. This requirement may be relaxed in future.
* @see [com.intellij.openapi.editor.InlayModel.addBlockElement]
*/
fun addBlockElement(offset: Int, relatesToPrecedingText: Boolean, showAbove: Boolean, priority: Int, presentation: InlayPresentation)
/**
* API can be changed in 2020.2!
*/
@ApiStatus.Experimental
fun addInlineElement(offset: Int, presentation: RootInlayPresentation<*>, constraints: HorizontalConstraints?)
/**
* API can be changed in 2020.2!
*/
@ApiStatus.Experimental
fun addBlockElement(logicalLine: Int, showAbove: Boolean, presentation: RootInlayPresentation<*>, constraints: BlockConstraints?)
}

View File

@@ -0,0 +1,76 @@
// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.codeInsight.hints
import com.intellij.codeInsight.hints.presentation.InlayPresentation
import org.jetbrains.annotations.ApiStatus
import org.jetbrains.annotations.Contract
import java.awt.Color
import java.awt.Point
import java.awt.event.MouseEvent
import javax.swing.Icon
@ApiStatus.Experimental
interface InlayPresentationFactory {
/**
* Text that can be used for elements that ARE valid syntax if they are pasted into file.
*/
@Contract(pure = true)
fun text(text: String) : InlayPresentation
/**
* Small text should be used for elements which text is not a valid syntax of the file, where this inlay is inserted.
* Should be used with [container] to be aligned and properly placed
* @return presentation that renders small text
*/
@Contract(pure = true)
fun smallText(text: String) : InlayPresentation
/**
* @return presentation that wraps existing with borders, background and rounded corners if set
* @param padding properties of space between [presentation] and border that is filled with background and has corners
* @param roundedCorners properties of rounded corners. If null, corners will have right angle
* @param background color of background, if null, no background will be rendered
* @param backgroundAlpha value from 0 to 1 of background opacity
*/
@Contract(pure = true)
fun container(
presentation: InlayPresentation,
padding: Padding? = null,
roundedCorners: RoundedCorners? = null,
background: Color? = null,
backgroundAlpha: Float = 0.55f
) : InlayPresentation
/**
* @return presentation that renders icon
*/
@Contract(pure = true)
fun icon(icon: Icon) : InlayPresentation
/**
* @return presentation with given mouse handlers
*/
@Contract(pure = true)
fun mouseHandling(base: InlayPresentation,
clickListener: ((MouseEvent, Point) -> Unit)?,
hoverListener: HoverListener?) : InlayPresentation
interface HoverListener {
fun onHover(event: MouseEvent, translated: Point)
fun onHoverFinished()
}
data class Padding(
val left: Int,
val right: Int,
val top: Int,
val bottom: Int
)
data class RoundedCorners(
val arcWidth: Int,
val arcHeight: Int
)
}

View File

@@ -0,0 +1,20 @@
// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.codeInsight.hints
import com.intellij.codeInsight.hints.presentation.InputHandler
import com.intellij.codeInsight.hints.presentation.PresentationListener
import com.intellij.codeInsight.hints.presentation.RootInlayPresentation
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.EditorCustomElementRenderer
import com.intellij.openapi.editor.Inlay
/**
* Renderer, that contains a group of [RootInlayPresentation] inside and shows them according to some positioning strategy (e. g. by priority)
*/
interface PresentationContainerRenderer<Constraints: Any> : EditorCustomElementRenderer, InputHandler {
fun setListener(listener: PresentationListener)
fun addOrUpdate(new: List<ConstrainedPresentation<*, Constraints>>, editor: Editor, factory: InlayPresentationFactory)
fun isAcceptablePlacement(placement: Inlay.Placement): Boolean
}

View File

@@ -8,9 +8,8 @@ import java.awt.Graphics2D
import java.awt.Rectangle
/**
* Building block of inlay view. Note, that you have to use [updateState] if your presentation has state to preserve it between passes.
* It's implementations are not expected to throw exceptions.
* Most useful methods for presentation creation are placed in PresentationFactory
* Building block of inlay view. It's implementations are not expected to throw exceptions.
* Most useful methods for presentation creation are placed in [com.intellij.codeInsight.hints.InlayPresentationFactory]
* If you implement new presentation, consider using [BasePresentation] as base class.
*/
@ApiStatus.Experimental

View File

@@ -0,0 +1,85 @@
// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.codeInsight.hints.presentation
import com.intellij.codeInsight.hints.ContentKey
import com.intellij.codeInsight.hints.InlayKey
import com.intellij.codeInsight.hints.InlayPresentationFactory
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.markup.TextAttributes
import java.awt.Dimension
import java.awt.Graphics2D
import java.awt.Point
import java.awt.Rectangle
import java.awt.event.MouseEvent
/**
* This class has some unavoidable problems: update happens on every pass, update is recursive and may be even unnecessary.
* New classes must not use this implementation!
*/
class RecursivelyUpdatingRootPresentation(private var current: InlayPresentation) : BasePresentation(), RootInlayPresentation<InlayPresentation> {
private var listener = MyPresentationListener()
init {
current.addListener(listener)
}
override fun update(
newPresentationContent: InlayPresentation,
editor: Editor,
factory: InlayPresentationFactory
): Boolean {
val changed = newPresentationContent.updateState(current)
if (!changed) return false
current.removeListener(listener)
current = newPresentationContent
listener = MyPresentationListener()
current.addListener(listener)
return changed
}
override val content: InlayPresentation
get() = current
override val key: ContentKey<InlayPresentation>
get() = KEY
// All other members are just delegation to underlying presentation
override val width: Int
get() = this.current.width
override val height: Int
get() = this.current.height
override fun paint(g: Graphics2D, attributes: TextAttributes) {
this.current.paint(g, attributes)
}
override fun toString(): String {
return this.current.toString()
}
override fun mouseClicked(event: MouseEvent, translated: Point) {
this.current.mouseClicked(event, translated)
}
override fun mouseMoved(event: MouseEvent, translated: Point) {
this.current.mouseMoved(event, translated)
}
override fun mouseExited() {
this.current.mouseExited()
}
companion object {
private val KEY: ContentKey<InlayPresentation> = InlayKey<Any, InlayPresentation>("recursive.update.root")
}
inner class MyPresentationListener : PresentationListener {
override fun contentChanged(area: Rectangle) {
this@RecursivelyUpdatingRootPresentation.fireContentChanged(area)
}
override fun sizeChanged(previous: Dimension, current: Dimension) {
this@RecursivelyUpdatingRootPresentation.fireSizeChanged(previous, current)
}
}
}

View File

@@ -0,0 +1,28 @@
// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.codeInsight.hints.presentation
import com.intellij.codeInsight.hints.ContentKey
import com.intellij.codeInsight.hints.InlayPresentationFactory
import com.intellij.openapi.editor.Editor
/**
* Root of the presentation tree, has explicit content ([Content])
* This method is responsible for construction of actual presentation and must do it lazily (due to performance reasons).
*/
interface RootInlayPresentation<Content : Any> : InlayPresentation {
/**
* Method is called on old presentation to apply updates to its content.
*
* This action should be FAST, it executes inside write action!
* This method is called only if [key]s of presentations are the same.
*
* @param newPresentationContent is a content of root of the NEW presentation
* @return true, iff something is really changed
*/
fun update(newPresentationContent: Content, editor: Editor, factory: InlayPresentationFactory): Boolean
val content: Content
val key: ContentKey<Content>
}

View File

@@ -0,0 +1,28 @@
// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.codeInsight.hints
import com.intellij.codeInsight.hints.presentation.VerticalListInlayPresentation
import com.intellij.openapi.editor.Inlay
class BlockInlayRenderer(
constrainedPresentations: Collection<ConstrainedPresentation<*, BlockConstraints>>
) : LinearOrderInlayRenderer<BlockConstraints>(
constrainedPresentations = constrainedPresentations,
createPresentation = { constrained ->
when (constrained.size) {
1 -> constrained.first().root
else -> VerticalListInlayPresentation(constrained.map { it.root })
}
},
comparator = COMPARISON
) {
override fun isAcceptablePlacement(placement: Inlay.Placement): Boolean {
return placement == Inlay.Placement.BELOW_LINE || placement == Inlay.Placement.ABOVE_LINE
}
companion object {
private val COMPARISON: (ConstrainedPresentation<*, BlockConstraints>) -> Int = { it.priority }
}
}

View File

@@ -1,27 +0,0 @@
// Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.codeInsight.hints
import com.intellij.codeInsight.hints.presentation.InlayPresentation
/**
* Component that represents stateful hint
* @param S state of the component
*/
abstract class HintComponent<S : Any>(
private var state: S
) {
// TODO lazy presentation
private var presentation : InlayPresentation = render(state)
abstract fun render(s: S) : InlayPresentation
// TODO just update? we may want to update hints's state partially
open fun shouldUpdate(newState: S): Boolean {
return state != newState
}
fun setState(newState: S) {
state = newState
presentation = render(state)
}
}

View File

@@ -0,0 +1,64 @@
// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.codeInsight.hints
import com.intellij.openapi.editor.Inlay
import gnu.trove.TIntHashSet
import gnu.trove.TIntObjectHashMap
/**
* Utility class to accumulate hints. Non thread-safe.
*/
class HintsBuffer {
val inlineHints = TIntObjectHashMap<MutableList<ConstrainedPresentation<*, HorizontalConstraints>>>()
val blockBelowHints = TIntObjectHashMap<MutableList<ConstrainedPresentation<*, BlockConstraints>>>()
val blockAboveHints = TIntObjectHashMap<MutableList<ConstrainedPresentation<*, BlockConstraints>>>()
fun mergeIntoThis(another: HintsBuffer) {
inlineHints.mergeIntoThis(another.inlineHints)
blockBelowHints.mergeIntoThis(another.blockBelowHints)
blockAboveHints.mergeIntoThis(another.blockAboveHints)
}
/**
* Counts all offsets of given [placement] which are not inside [other]
*/
fun countDisjointElements(other: TIntHashSet, placement: Inlay.Placement): Int {
val map = getMap(placement)
var count = 0
map.forEachKey {
if (it !in other) count++
true
}
return count
}
fun contains(offset: Int, placement: Inlay.Placement): Boolean {
return getMap(placement).contains(offset)
}
fun remove(offset: Int, placement: Inlay.Placement): MutableList<ConstrainedPresentation<*, *>>? {
return getMap(placement).remove(offset)
}
@Suppress("UNCHECKED_CAST")
private fun getMap(placement: Inlay.Placement) : TIntObjectHashMap<MutableList<ConstrainedPresentation<*, *>>> {
return when (placement) {
Inlay.Placement.INLINE -> inlineHints
Inlay.Placement.ABOVE_LINE -> blockAboveHints
Inlay.Placement.BELOW_LINE -> blockBelowHints
Inlay.Placement.AFTER_LINE_END -> TODO()
} as TIntObjectHashMap<MutableList<ConstrainedPresentation<*, *>>>
}
}
fun <V>TIntObjectHashMap<MutableList<V>>.mergeIntoThis(another: TIntObjectHashMap<MutableList<V>>) {
another.forEachEntry { otherOffset, otherList ->
val current = this[otherOffset]
if (current == null) {
put(otherOffset, otherList)
} else {
current.addAll(otherList)
}
true
}
}

View File

@@ -3,69 +3,225 @@ package com.intellij.codeInsight.hints
import com.intellij.codeHighlighting.EditorBoundHighlightingPass
import com.intellij.codeInsight.daemon.impl.analysis.HighlightingLevelManager
import com.intellij.codeInsight.hints.presentation.PresentationFactory
import com.intellij.codeInsight.hints.presentation.PresentationListener
import com.intellij.concurrency.JobLauncher
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.Inlay
import com.intellij.openapi.editor.InlayModel
import com.intellij.openapi.editor.impl.EditorImpl
import com.intellij.openapi.progress.ProgressIndicator
import com.intellij.openapi.util.Disposer
import com.intellij.openapi.util.Key
import com.intellij.psi.PsiElement
import com.intellij.psi.SyntaxTraverser
import com.intellij.util.Processor
import gnu.trove.TIntHashSet
import gnu.trove.TIntObjectHashMap
import org.jetbrains.annotations.NotNull
import java.awt.Dimension
import java.awt.Rectangle
import java.util.concurrent.ConcurrentLinkedQueue
import java.util.stream.IntStream
class InlayHintsPass(
val rootElement: PsiElement,
val collectors: List<CollectorWithSettings<out Any>>,
editor: Editor,
val settings: InlayHintsSettings
private val rootElement: PsiElement,
private val enabledCollectors: List<CollectorWithSettings<out Any>>,
private val editor: Editor
) : EditorBoundHighlightingPass(editor, rootElement.containingFile, true) {
private var allHints: HintsBuffer? = null
override fun doCollectInformation(progress: ProgressIndicator) {
if (!HighlightingLevelManager.getInstance(myFile.project).shouldHighlight(myFile)) return
val buffers = ConcurrentLinkedQueue<HintsBuffer>()
JobLauncher.getInstance().invokeConcurrentlyUnderProgress(
collectors,
enabledCollectors,
progress,
true,
false,
Processor { collector ->
// TODO [roman.ivanov] it is not good to create separate traverser here as there may be many hints providers
val traverser = SyntaxTraverser.psiTraverser(rootElement)
if (settings.hintsEnabled(collector.key, collector.language)) {
for (element in traverser.preOrderDfsTraversal()) {
if (!collector.collectHints(element, myEditor)) break
}
for (element in traverser.preOrderDfsTraversal()) {
if (!collector.collectHints(element, myEditor)) break
}
val hints = collector.sink.complete()
buffers.add(hints)
true
}
)
val iterator = buffers.iterator()
if (!iterator.hasNext()) return
val allHintsAccumulator = iterator.next()
for (hintsBuffer in iterator) {
allHintsAccumulator.mergeIntoThis(hintsBuffer)
}
allHints = allHintsAccumulator
}
override fun doApplyInformationToEditor() {
val element = rootElement
val startOffset = element.textOffset
val endOffset = element.textRange.endOffset
val inlayModel = myEditor.inlayModel
// Marked those inlays, that were managed by some provider
val existingHorizontalInlays: MarkList<Inlay<*>> = MarkList(inlayModel.getInlineElementsInRange(startOffset, endOffset))
val existingVerticalInlays: MarkList<Inlay<*>> = MarkList(inlayModel.getBlockElementsInRange(startOffset, endOffset))
for (collector in collectors) {
collector.applyToEditor(
myEditor,
existingHorizontalInlays,
existingVerticalInlays,
settings.hintsShouldBeShown(collector.key, collector.language)
)
}
disposeOrphanInlays(existingHorizontalInlays)
disposeOrphanInlays(existingVerticalInlays)
if (rootElement == myFile) {
applyCollected(allHints, rootElement, editor)
if (rootElement === myFile) {
InlayHintsPassFactory.putCurrentModificationStamp(myEditor, myFile)
}
}
private fun disposeOrphanInlays(inlays: MarkList<Inlay<*>>) {
// This may happen e. g. when extension of file is changed and providers that created inlay now not manage these inlays
inlays.iterateNonMarked { _, inlay ->
if (InlayHintsSinkImpl.isProvidersInlay(inlay)) {
inlay.dispose()
}
class InlayContentListener(private val inlay: Inlay<out PresentationContainerRenderer<*>>) : PresentationListener {
// TODO more precise redraw, requires changes in Inlay
override fun contentChanged(area: Rectangle) {
inlay.repaint()
}
override fun sizeChanged(previous: Dimension, current: Dimension) {
inlay.update()
}
}
}
companion object {
private const val BULK_CHANGE_THRESHOLD = 1000
private val MANAGED_KEY = Key.create<Boolean>("managed.inlay")
internal fun applyCollected(hints: HintsBuffer?,
element: PsiElement,
editor: Editor) {
val startOffset = element.textOffset
val endOffset = element.textRange.endOffset
val inlayModel = editor.inlayModel
val existingInlineInlays = inlayModel.getInlineElementsInRange(startOffset, endOffset, InlineInlayRenderer::class.java)
val existingBlockInlays: List<Inlay<out BlockInlayRenderer>> = inlayModel.getBlockElementsInRange(startOffset, endOffset,
BlockInlayRenderer::class.java)
val existingBlockAboveInlays = mutableListOf<Inlay<out PresentationContainerRenderer<*>>>()
val existingBlockBelowInlays = mutableListOf<Inlay<out PresentationContainerRenderer<*>>>()
for (inlay in existingBlockInlays) {
when (inlay.placement) {
Inlay.Placement.ABOVE_LINE -> existingBlockAboveInlays
Inlay.Placement.BELOW_LINE -> existingBlockBelowInlays
else -> throw IllegalStateException()
}.add(inlay)
}
val isBulk = shouldBeBulk(hints, existingInlineInlays, existingBlockAboveInlays, existingBlockBelowInlays)
val factory = PresentationFactory(editor as EditorImpl)
inlayModel.execute(isBulk) {
updateOrDispose(existingInlineInlays, hints, Inlay.Placement.INLINE, factory, editor)
updateOrDispose(existingBlockAboveInlays, hints, Inlay.Placement.ABOVE_LINE, factory, editor)
updateOrDispose(existingBlockBelowInlays, hints, Inlay.Placement.BELOW_LINE, factory, editor)
if (hints != null) {
addInlineHints(hints, inlayModel)
addBlockHints(inlayModel, hints.blockAboveHints, true)
addBlockHints(inlayModel, hints.blockBelowHints, false)
}
}
}
private fun postprocessInlay(inlay: Inlay<out PresentationContainerRenderer<*>>) {
inlay.renderer.setListener(InlayContentListener(inlay))
inlay.putUserData(MANAGED_KEY, true)
}
private fun addInlineHints(hints: HintsBuffer,
inlayModel: @NotNull InlayModel) {
hints.inlineHints.forEachEntry { offset, presentations ->
val renderer = InlineInlayRenderer(presentations)
val inlay = inlayModel.addInlineElement(offset, renderer) ?: return@forEachEntry false
postprocessInlay(inlay)
true
}
}
private fun addBlockHints(inlayModel: @NotNull InlayModel,
map: TIntObjectHashMap<MutableList<ConstrainedPresentation<*, BlockConstraints>>>,
showAbove: Boolean
) {
map.forEachEntry { offset, presentations ->
val renderer = BlockInlayRenderer(presentations)
val constraints = presentations.first().constraints
val inlay = inlayModel.addBlockElement(
offset,
constraints?.relatesToPrecedingText ?: true,
showAbove,
constraints?.priority ?: 0,
renderer
) ?: return@forEachEntry false
postprocessInlay(inlay)
showAbove
}
}
private fun shouldBeBulk(hints: HintsBuffer?,
existingInlineInlays: MutableList<Inlay<out InlineInlayRenderer>>,
existingBlockAboveInlays: MutableList<Inlay<out PresentationContainerRenderer<*>>>,
existingBlockBelowInlays: MutableList<Inlay<out PresentationContainerRenderer<*>>>): Boolean {
val totalChangesCount = when {
hints != null -> estimateChangesCountForPlacement(existingInlineInlays.offsets(), hints, Inlay.Placement.INLINE) +
estimateChangesCountForPlacement(existingBlockAboveInlays.offsets(), hints, Inlay.Placement.ABOVE_LINE) +
estimateChangesCountForPlacement(existingBlockBelowInlays.offsets(), hints, Inlay.Placement.BELOW_LINE)
else -> existingInlineInlays.size + existingBlockAboveInlays.size + existingBlockBelowInlays.size
}
return totalChangesCount > BULK_CHANGE_THRESHOLD
}
private fun List<Inlay<*>>.offsets(): IntStream = stream().mapToInt { it.offset }
private fun updateOrDispose(existing: List<Inlay<out PresentationContainerRenderer<*>>>,
hints: HintsBuffer?,
placement: Inlay.Placement,
factory: InlayPresentationFactory,
editor: Editor
) {
for (inlay in existing) {
val managed = inlay.getUserData(MANAGED_KEY) ?: continue
if (!managed) continue
val offset = inlay.offset
val elements = hints?.remove(offset, placement)
if (elements == null) {
Disposer.dispose(inlay)
continue
}
else {
inlay.renderer.addOrUpdate(elements, factory, placement, editor)
}
}
}
/**
* Estimates count of changes (removal, addition) for inlays
*/
fun estimateChangesCountForPlacement(existingInlayOffsets: IntStream,
collected: HintsBuffer,
placement: Inlay.Placement): Int {
var count = 0
val offsetsWithExistingHints = TIntHashSet()
for (offset in existingInlayOffsets) {
if (!collected.contains(offset, placement)) {
count++
}
else {
offsetsWithExistingHints.add(offset)
}
}
val elementsToCreate = collected.countDisjointElements(offsetsWithExistingHints, placement)
count += elementsToCreate
return count
}
private fun <Constraints : Any> PresentationContainerRenderer<Constraints>.addOrUpdate(
new: List<ConstrainedPresentation<*, *>>,
factory: InlayPresentationFactory,
placement: Inlay.Placement,
editor: Editor
) {
if (!isAcceptablePlacement(placement)) {
throw IllegalArgumentException()
}
@Suppress("UNCHECKED_CAST")
return addOrUpdate(new as List<ConstrainedPresentation<*, Constraints>>, editor, factory)
}
}
}

View File

@@ -28,12 +28,11 @@ class InlayHintsPassFactory : TextEditorHighlightingPassFactory, TextEditorHighl
val language = file.language
val collectors = HintUtils.getHintProvidersForLanguage(language, file.project)
.mapNotNull { it.getCollectorWrapperFor(file, editor, language) }
return InlayHintsPass(file, collectors, editor, settings)
.filter { settings.hintsShouldBeShown(it.key, language) }
return InlayHintsPass(file, collectors, editor)
}
companion object {
private val PSI_MODIFICATION_STAMP = Key.create<Long>("inlay.psi.modification.stamp")
fun forceHintsUpdateOnNextPass() {
for (editor in EditorFactory.getInstance().allEditors) {
editor.putUserData(PSI_MODIFICATION_STAMP, null)
@@ -43,6 +42,9 @@ class InlayHintsPassFactory : TextEditorHighlightingPassFactory, TextEditorHighl
}
}
@JvmStatic
private val PSI_MODIFICATION_STAMP = Key.create<Long>("inlay.psi.modification.stamp")
fun putCurrentModificationStamp(editor: Editor, file: PsiFile) {
editor.putUserData(PSI_MODIFICATION_STAMP, getCurrentModificationStamp(file))
}

View File

@@ -1,44 +1,23 @@
// Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.codeInsight.hints
import com.intellij.codeInsight.hints.presentation.InlayPresentation
import com.intellij.codeInsight.hints.presentation.PresentationListener
import com.intellij.codeInsight.hints.presentation.PresentationRenderer
import com.intellij.openapi.diagnostic.logger
import com.intellij.codeInsight.hints.presentation.RecursivelyUpdatingRootPresentation
import com.intellij.codeInsight.hints.presentation.RootInlayPresentation
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.Inlay
import com.intellij.openapi.editor.InlayModel
import com.intellij.openapi.editor.ex.util.EditorScrollingPositionKeeper
import com.intellij.openapi.util.Disposer
import com.intellij.openapi.util.Key
import com.intellij.openapi.editor.LogicalPosition
import com.intellij.util.SmartList
import gnu.trove.TIntObjectHashMap
import java.awt.Dimension
import java.awt.Rectangle
private sealed class InlayHint(val offset: Int, val presentation: InlayPresentation)
private class InlineElement(
offset: Int,
val relatesToPrecedingText: Boolean,
presentation: InlayPresentation
) : InlayHint(offset, presentation)
private class BlockElement(
offset: Int,
val relatesToPrecedingText: Boolean,
val showAbove: Boolean,
val priority: Int,
presentation: InlayPresentation
) : InlayHint(offset, presentation)
private class HintsAtOffset(var inlineElement: InlineElement?, var blockElement: BlockElement?)
class InlayHintsSinkImpl<T>(val key: SettingsKey<T>) : InlayHintsSink {
private val hints = TIntObjectHashMap<HintsAtOffset>()
class InlayHintsSinkImpl(val editor: Editor) : InlayHintsSink {
private val buffer = HintsBuffer()
override fun addInlineElement(offset: Int, relatesToPrecedingText: Boolean, presentation: InlayPresentation) {
addHint(InlineElement(offset, relatesToPrecedingText, presentation))
addInlineElement(offset, RecursivelyUpdatingRootPresentation(presentation), HorizontalConstraints(0, relatesToPrecedingText))
}
override fun addInlineElement(offset: Int, presentation: RootInlayPresentation<*>, constraints: HorizontalConstraints?) {
buffer.inlineHints.addCreatingListIfNeeded(offset, HorizontalConstrainedPresentation(presentation, constraints))
}
override fun addBlockElement(offset: Int,
@@ -46,142 +25,35 @@ class InlayHintsSinkImpl<T>(val key: SettingsKey<T>) : InlayHintsSink {
showAbove: Boolean,
priority: Int,
presentation: InlayPresentation) {
addHint(BlockElement(offset, relatesToPrecedingText, showAbove, priority, presentation))
val line = editor.offsetToLogicalPosition(offset).line
val root = RecursivelyUpdatingRootPresentation(presentation)
addBlockElement(line, showAbove, root, BlockConstraints(relatesToPrecedingText, priority)) // TODO here lines are applied
}
private fun addHint(hint: InlayHint) {
var hintsAtOffset = hints[hint.offset]
if (hintsAtOffset == null) {
hintsAtOffset = HintsAtOffset(null, null)
hints.put(hint.offset, hintsAtOffset)
}
when (hint) {
is InlineElement -> {
if (hintsAtOffset.inlineElement == null) {
hintsAtOffset.inlineElement = hint
} else {
logAtTheSameOffset(hint)
}
}
is BlockElement -> {
if (hintsAtOffset.blockElement == null) {
hintsAtOffset.blockElement = hint
} else {
logAtTheSameOffset(hint)
}
}
}
override fun addBlockElement(logicalLine: Int,
showAbove: Boolean,
presentation: RootInlayPresentation<*>,
constraints: BlockConstraints?) {
val map = if (showAbove) buffer.blockAboveHints else buffer.blockBelowHints
val offset = editor.logicalPositionToOffset(LogicalPosition(logicalLine, 0))
map.addCreatingListIfNeeded(offset, BlockConstrainedPresentation(presentation, constraints))
}
private fun logAtTheSameOffset(hint: InlayHint) {
LOG.warn("Hint added to the same offset: ${hint.offset} ${hint.presentation}")
}
/**
* This method called every time, when it is required to update hints even for disabled providers.
*/
fun applyToEditor(editor: Editor,
existingHorizontalInlays: MarkList<Inlay<*>>,
existingVerticalInlays: MarkList<Inlay<*>>,
isEnabled: Boolean) {
val inlayModel = editor.inlayModel
val isBulkChange = existingHorizontalInlays.size + hints.size() > BulkChangeThreshold
EditorScrollingPositionKeeper.perform(editor, true) {
editor.inlayModel.execute(isBulkChange) {
updateOrDeleteExistingHints(existingHorizontalInlays, existingVerticalInlays, isEnabled)
if (isEnabled) {
createNewHints(inlayModel)
}
}
}
hints.clear()
}
private fun createNewHints(inlayModel: InlayModel) {
hints.forEachEntry { offset, hints ->
hints.inlineElement?.let {
createNewHint(inlayModel, it, offset)
}
hints.blockElement?.let {
createNewHint(inlayModel, it, offset)
}
true
}
}
private fun createNewHint(inlayModel: InlayModel, hint: InlayHint, offset: Int) : Inlay<PresentationRenderer>? {
val presentation = hint.presentation
val presentationRenderer = PresentationRenderer(presentation)
val inlay = when (hint) {
is InlineElement -> inlayModel.addInlineElement(offset, hint.relatesToPrecedingText, presentationRenderer)
is BlockElement -> inlayModel.addBlockElement(
offset,
hint.relatesToPrecedingText,
hint.showAbove,
hint.priority,
presentationRenderer
)
} ?: return null
putSettingsKey(inlay, key)
presentation.addListener(InlayListener(inlay))
return inlay
}
class InlayListener(private val inlay: Inlay<PresentationRenderer>) : PresentationListener {
// TODO be more accurate during invalidation (requires changes in Inlay)
override fun contentChanged(area: Rectangle) = inlay.repaint()
override fun sizeChanged(previous: Dimension, current: Dimension) = inlay.update()
}
private fun updateOrDeleteExistingHints(
existingHorizontalInlays: MarkList<Inlay<*>>,
existingVerticalInlays: MarkList<Inlay<*>>,
isEnabled: Boolean
) {
updateOrDeleteExistingHints(existingHorizontalInlays, true, isEnabled)
updateOrDeleteExistingHints(existingVerticalInlays, false, isEnabled)
}
private fun updateOrDeleteExistingHints(existingInlays: MarkList<Inlay<*>>, isInline: Boolean, isEnabled: Boolean) {
existingInlays.iterateNonMarked { index, inlay ->
val inlayKey = getSettingsKey(inlay)
if (inlayKey != key) return@iterateNonMarked
val offset = inlay.offset
val hint = when (val hintsAtOffset = hints[offset]) {
null -> null
else -> when {
isInline -> hintsAtOffset.inlineElement
else -> hintsAtOffset.blockElement
}
}
if (hint == null || !isEnabled) {
Disposer.dispose(inlay)
}
else {
val newPresentation = hint.presentation
val renderer = inlay.renderer as PresentationRenderer
val previousPresentation = renderer.presentation
@Suppress("UNCHECKED_CAST")
newPresentation.addListener(InlayListener(inlay as Inlay<PresentationRenderer>))
renderer.presentation = newPresentation
if (newPresentation.updateState(previousPresentation)) {
newPresentation.fireUpdateEvent(previousPresentation.dimension())
}
hints.remove(offset)
existingInlays.mark(index)
}
}
internal fun complete(): HintsBuffer {
return buffer
}
companion object {
fun getSettingsKey(inlay: Inlay<*>): SettingsKey<*>? = inlay.getUserData(INLAY_KEY)
fun putSettingsKey(inlay: Inlay<*>, settingsKey: SettingsKey<*>) = inlay.putUserData(INLAY_KEY, settingsKey)
fun isProvidersInlay(inlay: Inlay<*>): Boolean = getSettingsKey(inlay) != null
private val INLAY_KEY: Key<SettingsKey<*>> = Key.create("INLAY_KEY")
private const val BulkChangeThreshold = 1000
@JvmField val LOG = logger<InlayHintsSinkImpl<*>>()
private fun <T : Any> TIntObjectHashMap<MutableList<ConstrainedPresentation<*, T>>>.addCreatingListIfNeeded(
offset: Int,
value: ConstrainedPresentation<*, T>
) {
var list = this[offset]
if (list == null) {
list = SmartList()
put(offset, list)
}
list.add(value)
}
}
}

View File

@@ -2,14 +2,15 @@
package com.intellij.codeInsight.hints
import com.intellij.codeInsight.hints.presentation.InlayPresentation
import com.intellij.codeInsight.hints.presentation.RootInlayPresentation
import com.intellij.configurationStore.deserializeInto
import com.intellij.configurationStore.serialize
import com.intellij.lang.Language
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.Inlay
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import com.intellij.psi.SyntaxTraverser
import com.intellij.util.SmartList
import java.awt.Dimension
import java.awt.Rectangle
@@ -32,7 +33,7 @@ fun <T : Any> ProviderWithSettings<T>.withSettingsCopy(): ProviderWithSettings<T
fun <T : Any> ProviderWithSettings<T>.getCollectorWrapperFor(file: PsiFile, editor: Editor, language: Language): CollectorWithSettings<T>? {
val key = provider.key
val sink = InlayHintsSinkImpl(key)
val sink = InlayHintsSinkImpl(editor)
val collector = provider.getCollectorFor(file, editor, settings, sink) ?: return null
return CollectorWithSettings(collector, key, language, sink)
}
@@ -58,32 +59,26 @@ class CollectorWithSettings<T : Any>(
val collector: InlayHintsCollector,
val key: SettingsKey<T>,
val language: Language,
val sink: InlayHintsSinkImpl<T>
val sink: InlayHintsSinkImpl
) {
fun collectHints(element: PsiElement, editor: Editor): Boolean {
return collector.collect(element, editor, sink)
}
fun applyToEditor(
editor: Editor,
existingHorizontalInlays: MarkList<Inlay<*>>,
existingVerticalInlays: MarkList<Inlay<*>>,
isEnabled: Boolean
) {
sink.applyToEditor(editor, existingHorizontalInlays, existingVerticalInlays, isEnabled)
}
/**
* Collects hints from the file and apply them to editor.
* Doesn't expect other hints in editor.
* Use only for settings preview.
*/
fun collectTraversingAndApply(editor: Editor, file: PsiFile, enabled: Boolean) {
val traverser = SyntaxTraverser.psiTraverser(file)
traverser.forEach {
collectHints(it, editor)
if (enabled) {
val traverser = SyntaxTraverser.psiTraverser(file)
traverser.forEach {
collectHints(it, editor)
}
}
val model = editor.inlayModel
val startOffset = file.textOffset
val endOffset = file.textRange.endOffset
val existingHorizontalInlays: MarkList<Inlay<*>> = MarkList(model.getInlineElementsInRange(startOffset, endOffset))
val existingVerticalInlays: MarkList<Inlay<*>> = MarkList(model.getBlockElementsInRange(startOffset, endOffset))
applyToEditor(editor, existingHorizontalInlays, existingVerticalInlays, enabled)
val buffer = sink.complete()
InlayHintsPass.applyCollected(buffer, file, editor)
}
}
@@ -99,4 +94,93 @@ fun InlayPresentation.fireUpdateEvent(previousDimension: Dimension) {
fireContentChanged()
}
fun InlayPresentation.dimension() = Dimension(width, height)
fun InlayPresentation.dimension() = Dimension(width, height)
private typealias ConstrPresent<C> = ConstrainedPresentation<*, C>
object InlayHintsUtils {
/**
* Function updates list of old presentations with new list, taking into account priorities.
* Both lists must be sorted.
*
* @return list of updated constrained presentations
*/
fun <Constraint: Any> produceUpdatedRootList(
new: List<ConstrPresent<Constraint>>,
old: List<ConstrPresent<Constraint>>,
editor: Editor,
factory: InlayPresentationFactory
): List<ConstrPresent<Constraint>> {
val updatedPresentations: MutableList<ConstrPresent<Constraint>> = SmartList()
// TODO [roman.ivanov]
// this function creates new list anyway, even if nothing from old presentations got updated,
// which makes us update list of presentations on every update (which should be relatively rare!)
// maybe I should really create new list only in case when anything get updated
val oldSize = old.size
val newSize = new.size
var oldIndex = 0
var newIndex = 0
// Simultaneous bypass of both lists and merging them to new one with element update
loop@
while (true) {
val newEl = new[newIndex]
val oldEl = old[oldIndex]
val newPriority = newEl.priority
val oldPriority = oldEl.priority
when {
newPriority > oldPriority -> {
updatedPresentations.add(oldEl)
oldIndex++
if (oldIndex == oldSize) {
break@loop
}
}
newPriority < oldPriority -> {
updatedPresentations.add(newEl)
newIndex++
if (newIndex == newSize) {
break@loop
}
}
else -> {
val oldRoot = oldEl.root
val newRoot = newEl.root
if (newRoot.key == oldRoot.key) {
oldRoot.updateIfSame(newRoot, editor, factory)
updatedPresentations.add(oldEl)
}
else {
updatedPresentations.add(newEl)
}
newIndex++
oldIndex++
if (newIndex == newSize || oldIndex == oldSize) {
break@loop
}
}
}
}
for (i in newIndex until newSize) {
updatedPresentations.add(new[i])
}
for (i in oldIndex until oldSize) {
updatedPresentations.add(old[i])
}
return updatedPresentations
}
/**
* @return true iff updated
*/
private fun <Content : Any>RootInlayPresentation<Content>.updateIfSame(
newPresentation: RootInlayPresentation<*>,
editor: Editor,
factory: InlayPresentationFactory
) : Boolean {
if (key != newPresentation.key) return false
@Suppress("UNCHECKED_CAST")
return update(newPresentation.content as Content, editor, factory)
}
}

View File

@@ -0,0 +1,27 @@
// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.codeInsight.hints
import com.intellij.codeInsight.hints.presentation.SequencePresentation
import com.intellij.openapi.editor.Inlay
class InlineInlayRenderer(
constrainedPresentations: Collection<ConstrainedPresentation<*, HorizontalConstraints>>
) : LinearOrderInlayRenderer<HorizontalConstraints>(
constrainedPresentations = constrainedPresentations,
createPresentation = { constrained ->
when (constrained.size) {
1 -> constrained.first().root
else -> SequencePresentation(constrained.map { it.root })
}
},
comparator = COMPARISON
) {
override fun isAcceptablePlacement(placement: Inlay.Placement): Boolean {
return placement == Inlay.Placement.INLINE
}
companion object {
private val COMPARISON: (ConstrainedPresentation<*, HorizontalConstraints>) -> Int = { it.priority }
}
}

View File

@@ -0,0 +1,113 @@
// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.codeInsight.hints
import com.intellij.codeInsight.hints.InlayHintsUtils.produceUpdatedRootList
import com.intellij.codeInsight.hints.presentation.InlayPresentation
import com.intellij.codeInsight.hints.presentation.PresentationListener
import com.intellij.codeInsight.hints.presentation.withTranslated
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.Inlay
import com.intellij.openapi.editor.markup.TextAttributes
import com.intellij.util.SmartList
import org.jetbrains.annotations.TestOnly
import java.awt.Graphics
import java.awt.Graphics2D
import java.awt.Point
import java.awt.Rectangle
import java.awt.event.MouseEvent
/**
* Renderer, that holds inside ordered list of [ConstrainedPresentation].
* Invariant: holds at least one presentation
*/
abstract class LinearOrderInlayRenderer<Constraint : Any>(
constrainedPresentations: Collection<ConstrainedPresentation<*, Constraint>>,
private val createPresentation: (List<ConstrainedPresentation<*, Constraint>>) -> InlayPresentation,
private val comparator: (ConstrainedPresentation<*, Constraint>) -> Int
) : PresentationContainerRenderer<Constraint> {
// Supposed to be changed rarely and rarely contains more than 1 element
private var presentations: List<ConstrainedPresentation<*, Constraint>> = SmartList(constrainedPresentations.sortedBy(comparator))
init {
assert(presentations.isNotEmpty())
}
private var cachedPresentation = createPresentation(presentations)
private var _listener: PresentationListener? = null
override fun addOrUpdate(new: List<ConstrainedPresentation<*, Constraint>>, editor: Editor, factory: InlayPresentationFactory) {
assert(new.isNotEmpty())
updateSorted(new.sortedBy(comparator), editor, factory)
}
override fun setListener(listener: PresentationListener) {
val oldListener = _listener
if (oldListener != null) {
cachedPresentation.removeListener(oldListener)
}
_listener = listener
cachedPresentation.addListener(listener)
}
private fun updateSorted(sorted: List<ConstrainedPresentation<*, Constraint>>,
editor: Editor,
factory: InlayPresentationFactory) {
// TODO [roman.ivanov] here can be handled 1 old to 1 new situation without complex algorithms and allocations
val tmp = produceUpdatedRootList(sorted, presentations, editor, factory)
presentations = tmp
_listener?.let {
cachedPresentation.removeListener(it)
}
cachedPresentation = createPresentation(presentations)
_listener?.let {
cachedPresentation.addListener(it)
}
cachedPresentation.fireContentChanged()
}
override fun paint(inlay: Inlay<*>, g: Graphics, targetRegion: Rectangle, textAttributes: TextAttributes) {
g as Graphics2D
g.withTranslated(targetRegion.x, targetRegion.y) {
cachedPresentation.paint(g, effectsIn(textAttributes))
}
}
override fun calcWidthInPixels(inlay: Inlay<*>): Int {
return cachedPresentation.width
}
// this should not be shown anywhere
override fun getContextMenuGroupId(inlay: Inlay<*>): String {
return "DummyActionGroup"
}
override fun mouseClicked(event: MouseEvent, translated: Point) {
cachedPresentation.mouseClicked(event, translated)
}
override fun mouseMoved(event: MouseEvent, translated: Point) {
cachedPresentation.mouseMoved(event, translated)
}
override fun mouseExited() {
cachedPresentation.mouseExited()
}
override fun toString(): String {
return cachedPresentation.toString()
}
@TestOnly
fun getConstrainedPresentations(): List<ConstrainedPresentation<*, Constraint>> = presentations
companion object {
fun effectsIn(attributes: TextAttributes): TextAttributes {
val result = TextAttributes()
result.effectType = attributes.effectType
result.effectColor = attributes.effectColor
return result
}
}
}

View File

@@ -1,36 +0,0 @@
// Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.codeInsight.hints
import java.util.*
class MarkList<T>(private val items: List<T>) : Iterable<T> {
private val myMarked = BitSet(items.size)
operator fun get(index: Int) : T = items[index]
val size: Int
get() = items.size
override fun iterator(): Iterator<T> = items.iterator()
fun mark(index: Int) {
myMarked[index] = true
}
fun mark(index: Int, value: Boolean) {
myMarked[index] = value
}
fun marked(index: Int): Boolean {
return myMarked[index]
}
fun iterateNonMarked(consumer: (Int, T) -> Unit) {
var i = myMarked.nextClearBit(0)
val itemsSize = items.size
while (i in 0 until itemsSize) {
consumer(i, items[i])
i = myMarked.nextClearBit(i + 1)
}
}
}

View File

@@ -0,0 +1,109 @@
// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.codeInsight.hints.presentation
import com.intellij.codeInsight.hints.InlayPresentationFactory
import com.intellij.openapi.editor.markup.TextAttributes
import com.intellij.util.ui.GraphicsUtil
import org.jetbrains.annotations.ApiStatus
import java.awt.Color
import java.awt.Graphics2D
import java.awt.Point
import java.awt.event.MouseEvent
/**
* Presentation that wraps existing with borders, background and rounded corners if set.
* @see com.intellij.codeInsight.hints.InlayPresentationFactory.container
*/
@ApiStatus.Experimental
class ContainerInlayPresentation(
presentation: InlayPresentation,
private val padding: InlayPresentationFactory.Padding? = null,
private val roundedCorners: InlayPresentationFactory.RoundedCorners? = null,
private val background: Color? = null,
private val backgroundAlpha: Float = 0.55f
) : StaticDelegatePresentation(presentation) {
private var presentationIsUnderCursor: Boolean = false
override val width: Int
get() = leftInset + presentation.width + rightInset
override val height: Int
get() = topInset + presentation.height + bottomInset
override fun paint(g: Graphics2D, attributes: TextAttributes) {
if (background != null) {
val preservedBackground = g.background
g.color = background
if (roundedCorners != null) {
val (arcWidth, arcHeight) = roundedCorners
fillRoundedRectangle(g, height, width, arcWidth, arcHeight, backgroundAlpha)
} else {
g.fillRect(0, 0, width, height)
}
g.color = preservedBackground
}
g.withTranslated(leftInset, topInset) {
presentation.paint(g, attributes)
}
}
override fun mouseClicked(event: MouseEvent, translated: Point) {
handleMouse(translated) { point ->
presentation.mouseClicked(event, point)
}
}
override fun mouseMoved(event: MouseEvent, translated: Point) {
handleMouse(translated) { point ->
presentation.mouseClicked(event, point)
}
}
override fun mouseExited() {
try {
presentation.mouseExited()
}
finally {
presentationIsUnderCursor = false
}
}
private fun handleMouse(
original: Point,
action: (Point) -> Unit
) {
val x = original.x
val y = original.y
if (!isInInnerBounds(x, y)) {
if (presentationIsUnderCursor) {
presentation.mouseExited()
presentationIsUnderCursor = false
}
return
}
val translated = original.translateNew(-leftInset, -topInset)
action(translated)
}
private fun isInInnerBounds(x: Int, y: Int): Boolean {
return x >= leftInset && x < leftInset + presentation.width && y >= topInset && y < topInset + presentation.height
}
private val leftInset: Int
get() = padding?.left ?: 0
private val rightInset: Int
get() = padding?.right ?: 0
private val topInset: Int
get() = padding?.top ?: 0
private val bottomInset: Int
get() = padding?.bottom ?: 0
private fun fillRoundedRectangle(g: Graphics2D, height: Int, width: Int, arcWidth: Int, arcHeight: Int, backgroundAlpha: Float) {
val config = GraphicsUtil.setupAAPainting(g)
GraphicsUtil.paintWithAlpha(g, backgroundAlpha)
g.fillRoundRect(0, 0, width, height, arcWidth, arcHeight)
config.restore()
}
}

View File

@@ -0,0 +1,53 @@
// Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.codeInsight.hints.presentation
import com.intellij.codeInsight.hints.PresentationContainerRenderer
import com.intellij.openapi.editor.event.EditorMouseEvent
import com.intellij.openapi.editor.event.EditorMouseEventArea
import com.intellij.openapi.editor.event.EditorMouseListener
import com.intellij.openapi.editor.event.EditorMouseMotionListener
import java.awt.Point
/**
* Global mouse listeners that provide events to inlay hints at mouse coordinates.
*/
class InlayEditorMouseListener : EditorMouseListener {
override fun mouseClicked(e: EditorMouseEvent) {
if (e.isConsumed) return
val editor = e.editor
val event = e.mouseEvent
if (editor.getMouseEventArea(event) != EditorMouseEventArea.EDITING_AREA) return
val point = event.point
val inlay = editor.inlayModel.getElementAt(point, PresentationContainerRenderer::class.java) ?: return
val bounds = inlay.bounds ?: return
val inlayPoint = Point(bounds.x, bounds.y)
val translated = Point(event.x - inlayPoint.x, event.y - inlayPoint.y)
inlay.renderer.mouseClicked(event, translated)
}
}
class InlayEditorMouseMotionListener : EditorMouseMotionListener {
private var activeContainer: PresentationContainerRenderer<*>? = null
override fun mouseMoved(e: EditorMouseEvent) {
if (e.isConsumed) return
val editor = e.editor
val event = e.mouseEvent
if (editor.getMouseEventArea(event) != EditorMouseEventArea.EDITING_AREA) {
activeContainer?.mouseExited()
activeContainer = null
return
}
val inlay = editor.inlayModel.getElementAt(event.point, PresentationContainerRenderer::class.java)
val container = inlay?.renderer
if (activeContainer != container) {
activeContainer?.mouseExited()
activeContainer = container
}
if (container == null) return
val bounds = inlay.bounds ?: return
val inlayPoint = Point(bounds.x, bounds.y)
val translated = Point(event.x - inlayPoint.x, event.y - inlayPoint.y)
container.mouseMoved(event, translated)
}
}

View File

@@ -17,7 +17,7 @@ class InsetPresentation(
val top: Int = 0,
val down: Int = 0
) : StaticDelegatePresentation(presentation) {
private var presentationUnderCursor: InlayPresentation? = null
private var isPresentationUnderCursor = false
override val width: Int
get() = presentation.width + left + right
@@ -36,12 +36,13 @@ class InsetPresentation(
) {
val x = original.x
val y = original.y
if (x < left || x >= left + presentation.width || y < top || y >= top + presentation.height) {
if (presentationUnderCursor != null) {
presentationUnderCursor?.mouseExited()
presentationUnderCursor = null
return
val cursorIsOutOfBounds = x < left || x >= left + presentation.width || y < top || y >= top + presentation.height
if (cursorIsOutOfBounds) {
if (isPresentationUnderCursor) {
presentation.mouseExited()
isPresentationUnderCursor = false
}
return
}
val translated = original.translateNew(-left, -top)
action(presentation, translated)
@@ -55,20 +56,12 @@ class InsetPresentation(
override fun mouseMoved(event: MouseEvent, translated: Point) {
handleMouse(translated) { presentation, point ->
if (presentation != presentationUnderCursor) {
presentationUnderCursor?.mouseExited()
presentationUnderCursor = presentation
}
presentation.mouseMoved(event, point)
}
}
override fun mouseExited() {
try {
presentationUnderCursor?.mouseExited()
}
finally {
presentationUnderCursor = null
}
presentation.mouseExited()
isPresentationUnderCursor = false
}
}

View File

@@ -0,0 +1,32 @@
// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.codeInsight.hints.presentation
import com.intellij.codeInsight.hints.InlayPresentationFactory
import org.jetbrains.annotations.ApiStatus
import java.awt.Point
import java.awt.event.MouseEvent
/**
* Presentation, that allow to setup reaction to mouse actions.
*/
@ApiStatus.Experimental
class MouseHandlingPresentation(
presentation: InlayPresentation,
private val clickListener: ((MouseEvent, Point) -> Unit)?,
private val hoverListener: InlayPresentationFactory.HoverListener?
) : StaticDelegatePresentation(presentation) {
override fun mouseClicked(event: MouseEvent, translated: Point) {
super.mouseClicked(event, translated)
clickListener?.invoke(event, translated)
}
override fun mouseMoved(event: MouseEvent, translated: Point) {
super.mouseMoved(event, translated)
hoverListener?.onHover(event, translated)
}
override fun mouseExited() {
super.mouseExited()
hoverListener?.onHoverFinished()
}
}

View File

@@ -1,6 +1,7 @@
// Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.codeInsight.hints.presentation
import com.intellij.codeInsight.hints.InlayPresentationFactory
import java.awt.Point
import java.awt.event.MouseEvent
@@ -9,20 +10,15 @@ import java.awt.event.MouseEvent
*/
class OnHoverPresentation(
presentation: InlayPresentation,
val onHoverListener: PresentationFactory.HoverListener) : StaticDelegatePresentation(presentation) {
var isInside = false
private val onHoverListener: InlayPresentationFactory.HoverListener) : StaticDelegatePresentation(presentation) {
override fun mouseMoved(event: MouseEvent, translated: Point) {
super.mouseMoved(event, translated)
if (!isInside) {
isInside = true
onHoverListener.onHover(event)
}
onHoverListener.onHover(event, translated)
}
override fun mouseExited() {
super.mouseExited()
isInside = false
onHoverListener.onHoverFinished()
}
}

View File

@@ -4,6 +4,7 @@ package com.intellij.codeInsight.hints.presentation
import com.intellij.codeInsight.hint.HintManager
import com.intellij.codeInsight.hint.HintManagerImpl
import com.intellij.codeInsight.hint.HintUtil
import com.intellij.codeInsight.hints.InlayPresentationFactory
import com.intellij.ide.ui.AntialiasingType
import com.intellij.openapi.command.CommandProcessor
import com.intellij.openapi.editor.DefaultLanguageHighlighterColors
@@ -28,14 +29,15 @@ import java.awt.event.MouseEvent
import java.awt.font.FontRenderContext
import java.util.*
import javax.swing.Icon
import kotlin.math.ceil
import kotlin.math.max
class PresentationFactory(private val editor: EditorImpl) {
/**
* Smaller text, than editor, required to be wrapped with [roundWithBackground]
*/
/**
* Users of InlayHintsFactory should use interface instead
*/
class PresentationFactory(private val editor: EditorImpl) : InlayPresentationFactory {
@Contract(pure = true)
fun smallText(text: String): InlayPresentation {
override fun smallText(text: String): InlayPresentation {
val fontData = getFontData(editor)
val plainFont = fontData.font
val width = editor.contentComponent.getFontMetrics(plainFont).stringWidth(text)
@@ -49,11 +51,28 @@ class PresentationFactory(private val editor: EditorImpl) {
return withInlayAttributes(textWithoutBox)
}
/**
* Text, that is not expected to be drawn with rounding, the same font size as in editor.
*/
override fun container(
presentation: InlayPresentation,
padding: InlayPresentationFactory.Padding?,
roundedCorners: InlayPresentationFactory.RoundedCorners?,
background: Color?,
backgroundAlpha: Float
): InlayPresentation {
return ContainerInlayPresentation(presentation, padding, roundedCorners, background, backgroundAlpha)
}
@Contract(pure = true)
fun text(text: String): InlayPresentation {
override fun mouseHandling(
base: InlayPresentation,
clickListener: ((MouseEvent, Point) -> Unit)?,
hoverListener: InlayPresentationFactory.HoverListener?
): InlayPresentation {
return MouseHandlingPresentation(base, clickListener, hoverListener)
}
@Contract(pure = true)
override fun text(text: String): InlayPresentation {
val font = editor.colorsScheme.getFont(EditorFontType.PLAIN)
val width = editor.contentComponent.getFontMetrics(font).stringWidth(text)
val ascent = editor.ascent
@@ -75,6 +94,7 @@ class PresentationFactory(private val editor: EditorImpl) {
* Intended to be used with [smallText]
*/
@Contract(pure = true)
@Deprecated(message = "", replaceWith = ReplaceWith("container"))
fun roundWithBackground(base: InlayPresentation): InlayPresentation {
val rounding = withInlayAttributes(RoundWithBackgroundPresentation(
InsetPresentation(
@@ -93,27 +113,7 @@ class PresentationFactory(private val editor: EditorImpl) {
}
@Contract(pure = true)
fun icon(icon: Icon): IconPresentation = IconPresentation(icon, editor.component)
@Contract(pure = true)
fun withTooltip(tooltip: String, base: InlayPresentation): InlayPresentation = when {
tooltip.isEmpty() -> base
else -> {
var hint: LightweightHint? = null
onHover(base, object : HoverListener {
override fun onHover(event: MouseEvent) {
if (hint?.isVisible != true) {
hint = showTooltip(editor, event, tooltip)
}
}
override fun onHoverFinished() {
hint?.hide()
hint = null
}
})
}
}
override fun icon(icon: Icon): IconPresentation = IconPresentation(icon, editor.component)
@Contract(pure = true)
fun folding(placeholder: InlayPresentation, unwrapAction: () -> InlayPresentation): InlayPresentation {
@@ -162,10 +162,6 @@ class PresentationFactory(private val editor: EditorImpl) {
return seq(prefixExposed, content, suffixExposed)
}
private fun flipState() {
TODO("not implemented")
}
@Contract(pure = true)
fun matchingBraces(left: InlayPresentation, right: InlayPresentation): Pair<InlayPresentation, InlayPresentation> {
val (leftMatching, rightMatching) = matching(listOf(left, right))
@@ -188,8 +184,8 @@ class PresentationFactory(private val editor: EditorImpl) {
decorator: (InlayPresentation) -> InlayPresentation): List<InlayPresentation> {
val forwardings = presentations.map { DynamicDelegatePresentation(it) }
return forwardings.map {
onHover(it, object : HoverListener {
override fun onHover(event: MouseEvent) {
onHover(it, object : InlayPresentationFactory.HoverListener {
override fun onHover(event: MouseEvent, translated: Point) {
for ((index, forwarding) in forwardings.withIndex()) {
forwarding.delegate = decorator(presentations[index])
}
@@ -204,17 +200,11 @@ class PresentationFactory(private val editor: EditorImpl) {
}
}
interface HoverListener {
fun onHover(event: MouseEvent)
fun onHoverFinished()
}
/**
* @see OnHoverPresentation
*/
@Contract(pure = true)
fun onHover(base: InlayPresentation, onHoverListener: HoverListener): InlayPresentation =
fun onHover(base: InlayPresentation, onHoverListener: InlayPresentationFactory.HoverListener): InlayPresentation =
OnHoverPresentation(base, onHoverListener)
/**
@@ -294,10 +284,6 @@ class PresentationFactory(private val editor: EditorImpl) {
return SequencePresentation(seq)
}
@Contract(pure = true)
fun rounding(arcWidth: Int, arcHeight: Int, base: InlayPresentation): InlayPresentation =
RoundPresentation(base, arcWidth, arcHeight)
private fun attributes(base: InlayPresentation, transformer: (TextAttributes) -> TextAttributes): AttributesTransformerPresentation =
AttributesTransformerPresentation(base, transformer)
@@ -364,8 +350,8 @@ class PresentationFactory(private val editor: EditorImpl) {
val context = getCurrentContext(editor)
metrics = FontInfo.getFontMetrics(font, context)
// We assume this will be a better approximation to a real line height for a given font
lineHeight = Math.ceil(font.createGlyphVector(context, "Albpq@").visualBounds.height).toInt()
baseline = Math.ceil(font.createGlyphVector(context, "Alb").visualBounds.height).toInt()
lineHeight = ceil(font.createGlyphVector(context, "Albpq@").visualBounds.height).toInt()
baseline = ceil(font.createGlyphVector(context, "Alb").visualBounds.height).toInt()
}
fun isActual(editor: Editor, familyName: String, size: Int): Boolean {

View File

@@ -1,6 +1,7 @@
// Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.codeInsight.hints.presentation
import com.intellij.codeInsight.hints.LinearOrderInlayRenderer
import com.intellij.openapi.editor.EditorCustomElementRenderer
import com.intellij.openapi.editor.Inlay
import com.intellij.openapi.editor.markup.TextAttributes
@@ -12,7 +13,7 @@ class PresentationRenderer(var presentation: InlayPresentation) : EditorCustomEl
override fun paint(inlay: Inlay<*>, g: Graphics, targetRegion: Rectangle, textAttributes: TextAttributes) {
g as Graphics2D
g.withTranslated(targetRegion.x, targetRegion.y) {
presentation.paint(g, effectsIn(textAttributes))
presentation.paint(g, LinearOrderInlayRenderer.effectsIn(textAttributes))
}
}
@@ -21,17 +22,12 @@ class PresentationRenderer(var presentation: InlayPresentation) : EditorCustomEl
return presentation.width
}
companion object {
fun effectsIn(attributes: TextAttributes): TextAttributes {
val result = TextAttributes()
result.effectType = attributes.effectType
result.effectColor = attributes.effectColor
return result
}
}
// this should not be shown anywhere
override fun getContextMenuGroupId(inlay: Inlay<*>): String {
return "DummyActionGroup"
}
override fun toString(): String {
return presentation.toString()
}
}

View File

@@ -9,6 +9,11 @@ import java.awt.Point
import java.awt.Rectangle
import java.awt.event.MouseEvent
/**
* Represents list of presentations placed without any margin, aligned to top.
* Must not be empty
* @param presentations list of presentations, must not be changed from outside as there is no defensive copying
*/
class SequencePresentation(val presentations: List<InlayPresentation>) : BasePresentation() {
init {
if (presentations.isEmpty()) throw IllegalArgumentException()
@@ -23,7 +28,9 @@ class SequencePresentation(val presentations: List<InlayPresentation>) : BasePre
}
override var width: Int = 0
private set
override var height: Int = 0
private set
init {
calcDimensions()
@@ -56,6 +63,11 @@ class SequencePresentation(val presentations: List<InlayPresentation>) : BasePre
for (presentation in presentations) {
val presentationWidth = presentation.width
if (x < xOffset + presentationWidth) {
if (y > presentation.height) { // out of presentation
changePresentationUnderCursor(null)
return
}
changePresentationUnderCursor(presentation)
val translated = original.translateNew(-xOffset, 0)
action(presentation, translated)
return
@@ -72,21 +84,12 @@ class SequencePresentation(val presentations: List<InlayPresentation>) : BasePre
override fun mouseMoved(event: MouseEvent, translated: Point) {
handleMouse(translated) { presentation, point ->
if (presentation != presentationUnderCursor) {
presentationUnderCursor?.mouseExited()
presentationUnderCursor = presentation
}
presentation.mouseMoved(event, point)
}
}
override fun mouseExited() {
try {
presentationUnderCursor?.mouseExited()
}
finally {
presentationUnderCursor = null
}
changePresentationUnderCursor(null)
}
override fun updateState(previousPresentation: InlayPresentation): Boolean {
@@ -104,7 +107,14 @@ class SequencePresentation(val presentations: List<InlayPresentation>) : BasePre
override fun toString(): String = presentations.joinToString(" ", "[", "]") { "$it" }
inner class InternalListener(private val currentPresentation: InlayPresentation) : PresentationListener {
private fun changePresentationUnderCursor(presentation: InlayPresentation?) {
if (presentationUnderCursor != presentation) {
presentationUnderCursor?.mouseExited()
presentationUnderCursor = presentation
}
}
private inner class InternalListener(private val currentPresentation: InlayPresentation) : PresentationListener {
override fun contentChanged(area: Rectangle) {
area.add(shiftOfCurrent(), 0)
this@SequencePresentation.fireContentChanged(area)

View File

@@ -9,4 +9,9 @@ data class SpacePresentation(override var width: Int, override var height: Int)
}
override fun toString(): String = " "
override fun updateState(previousPresentation: InlayPresentation): Boolean {
if (previousPresentation !is SpacePresentation) return true
return width != previousPresentation.width || height != previousPresentation.height
}
}

View File

@@ -0,0 +1,131 @@
// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.codeInsight.hints.presentation
import com.intellij.codeInsight.hints.dimension
import com.intellij.openapi.editor.markup.TextAttributes
import java.awt.Dimension
import java.awt.Graphics2D
import java.awt.Point
import java.awt.Rectangle
import java.awt.event.MouseEvent
/**
* Intended to store presentations inside block inlay. Aligned to left.
* Not expected to contain a lot of presentations
* Must not be empty.
* @param presentations list of presentations, must not be changed from outside as there is no defensive copying
*/
class VerticalListInlayPresentation(
val presentations: List<InlayPresentation>
) : BasePresentation() {
override var width: Int = 0
private set
override var height: Int = 0
private set
var presentationUnderCursor: InlayPresentation? = null
init {
calcDimensions()
for (presentation in presentations) {
presentation.addListener(InternalListener(presentation))
}
}
override fun paint(g: Graphics2D, attributes: TextAttributes) {
var yOffset = 0
try {
for (presentation in presentations) {
presentation.paint(g, attributes)
yOffset += presentation.height
g.translate(0, presentation.height)
}
}
finally {
g.translate(0, -yOffset)
}
}
override fun mouseClicked(event: MouseEvent, translated: Point) {
handleMouse(translated) { presentation, point ->
presentation.mouseClicked(event, point)
}
}
override fun mouseMoved(event: MouseEvent, translated: Point) {
handleMouse(translated) { presentation, point ->
presentation.mouseMoved(event, point)
}
}
override fun mouseExited() {
changePresentationUnderCursor(null)
}
fun calcDimensions() {
width = presentations.maxBy { it.width }!!.width
height = presentations.sumBy { it.height }
}
private fun handleMouse(
original: Point,
action: (InlayPresentation, Point) -> Unit
) {
val x = original.x
val y = original.y
if (x < 0 || x >= width || y < 0 || y >= height) return
var yOffset = 0
for (presentation in presentations) {
val presentationHeight = presentation.height
if (y < yOffset + presentationHeight && x < presentation.width) {
changePresentationUnderCursor(presentation)
val translated = original.translateNew(0, -yOffset)
action(presentation, translated)
return
}
yOffset += presentationHeight
}
}
private fun changePresentationUnderCursor(presentation: InlayPresentation?) {
if (presentationUnderCursor != presentation) {
presentationUnderCursor?.mouseExited()
presentationUnderCursor = presentation
}
}
override fun toString(): String {
return presentations.joinToString("\n")
}
// TODO area is incorrect, for now rely that all hint's area will be repainted
private inner class InternalListener(private val currentPresentation: InlayPresentation) : PresentationListener {
override fun contentChanged(area: Rectangle) {
area.add(shiftOfCurrent(), 0)
this@VerticalListInlayPresentation.fireContentChanged(area)
}
override fun sizeChanged(previous: Dimension, current: Dimension) {
val old = dimension()
calcDimensions()
val new = dimension()
this@VerticalListInlayPresentation.fireSizeChanged(old, new)
}
private fun shiftOfCurrent(): Int {
var shift = 0
for (presentation in presentations) {
if (presentation === currentPresentation) {
return shift
}
shift += presentation.height
}
throw IllegalStateException()
}
}
}

View File

@@ -1,55 +0,0 @@
// Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.codeInsight.hints.presentation
import com.intellij.openapi.editor.event.EditorMouseEvent
import com.intellij.openapi.editor.event.EditorMouseEventArea
import com.intellij.openapi.editor.event.EditorMouseListener
import com.intellij.openapi.editor.event.EditorMouseMotionListener
import java.awt.Point
/**
* Global mouse listeners, that provide events to inlay hints at mouse coordinates.
*/
class InlayEditorMouseListener : EditorMouseListener {
override fun mouseClicked(e: EditorMouseEvent) {
if (!e.isConsumed) {
val editor = e.editor
val event = e.mouseEvent
if (editor.getMouseEventArea(event) != EditorMouseEventArea.EDITING_AREA) return
val point = event.point
val inlay = editor.inlayModel.getElementAt(point, PresentationRenderer::class.java) ?: return
val bounds = inlay.bounds ?: return
val inlayPoint = Point(bounds.x, bounds.y)
val translated = Point(event.x - inlayPoint.x, event.y - inlayPoint.y)
inlay.renderer.presentation.mouseClicked(event, translated)
}
}
}
class InlayEditorMouseMotionListener : EditorMouseMotionListener {
private var activePresentation: InlayPresentation? = null
override fun mouseMoved(e: EditorMouseEvent) {
if (!e.isConsumed) {
val editor = e.editor
val event = e.mouseEvent
// TODO here also may be handling of ESC key
if (editor.getMouseEventArea(event) != EditorMouseEventArea.EDITING_AREA) {
activePresentation?.mouseExited()
return
}
val inlay = editor.inlayModel.getElementAt(event.point, PresentationRenderer::class.java)
val presentation = inlay?.renderer?.presentation
if (activePresentation != presentation) {
activePresentation?.mouseExited()
activePresentation = presentation
}
if (presentation != null) {
val bounds = inlay.bounds ?: return
val inlayPoint = Point(bounds.x, bounds.y)
val translated = Point(event.x - inlayPoint.x, event.y - inlayPoint.y)
presentation.mouseMoved(event, translated)
}
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 227 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 186 B

View File

@@ -0,0 +1,47 @@
// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.codeInsight.hints
import com.intellij.openapi.editor.Inlay
import junit.framework.TestCase
import java.util.stream.IntStream
class BulkEstimateTest : TestCase() {
fun testAddToExisting() = checkChanges(
existing = intArrayOf(2, 4, 5, 6),
collected = intArrayOf(1, 2, 3, 4, 5, 6),
expectedChanges = 2
)
fun testRemoveExisting() = checkChanges(
existing = intArrayOf(1, 2, 3, 4, 5, 6),
collected = intArrayOf(2, 4, 5),
expectedChanges = 3
)
fun testRemoveAndAdd() = checkChanges(
existing = intArrayOf(1, 2, 3, 4, 5, 6),
collected = intArrayOf(3, 4, 7, 8),
expectedChanges = 6
)
private fun checkChanges(collected: IntArray, existing: IntArray, expectedChanges: Int) {
val buffer = HintsBuffer()
addInline(buffer, *collected)
val actualChangesCount = InlayHintsPass.estimateChangesCountForPlacement(
existingInlayOffsets = IntStream.of(*existing),
collected = buffer,
placement = Inlay.Placement.INLINE
)
assertEquals(expectedChanges, actualChangesCount)
}
private fun addInline(buffer: HintsBuffer, vararg offsets: Int) {
for (offset in offsets) {
addInline(buffer, offset)
}
}
private fun addInline(buffer: HintsBuffer, offset: Int) {
buffer.inlineHints.put(offset, mutableListOf(HorizontalConstrainedPresentation(TestRootPresentation(1), null)))
}
}

View File

@@ -1,9 +1,10 @@
// Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.codeInsight.hints
import com.intellij.codeInsight.hints.presentation.RecursivelyUpdatingRootPresentation
import com.intellij.codeInsight.hints.presentation.RootInlayPresentation
import com.intellij.codeInsight.hints.presentation.SpacePresentation
import com.intellij.lang.Language
import com.intellij.openapi.components.ServiceManager
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.Inlay
import com.intellij.openapi.progress.DumbProgressIndicator
@@ -13,65 +14,153 @@ import com.intellij.testFramework.fixtures.BasePlatformTestCase
class InlayPassTest : BasePlatformTestCase() {
private val noSettings = SettingsKey<NoSettings>("no")
fun testBlockAndInlineElementMayBeAtSameOffset() {
override fun setUp() {
super.setUp()
myFixture.configureByText("file.java", "class A{ }")
val sink = InlayHintsSinkImpl(noSettings)
sink.addBlockElement(5, true, true, 0, SpacePresentation(0, 0))
sink.addInlineElement(5, true, SpacePresentation(10, 10))
val editor = myFixture.editor
sink.applyToEditor(editor, MarkList(emptyList()), MarkList(emptyList()), true)
assertEquals(1, inlineElements.size)
assertEquals(1, blockElements.size)
assertEquals(5, inlineElements.first().offset)
assertEquals(5, blockElements.first().offset)
}
fun testTurnedOffHintsDisappear() {
myFixture.configureByText("file.java", "class A{ }")
val sink = InlayHintsSinkImpl(noSettings)
sink.addBlockElement(5, true, true, 0, SpacePresentation(0, 0))
sink.addInlineElement(5, true, SpacePresentation(10, 10))
val editor = myFixture.editor
sink.applyToEditor(editor, inlineElements, blockElements, true)
sink.addInlineElement(2, true, SpacePresentation(10, 10))
sink.applyToEditor(editor, inlineElements, blockElements, false)
assertEquals(0, inlineElements.size)
assertEquals(0, blockElements.size)
createPass(listOf(createOneOffCollector {
it.addBlockElement(0, true, TestRootPresentation(0), null)
it.addInlineElement(5, TestRootPresentation(1), null)
})).collectAndApply()
assertEquals(2, allHintsCount)
createPass(emptyList()).collectAndApply()
assertEquals(0, allHintsCount)
}
fun testPassRemovesNonRelatedInlays() {
myFixture.configureByText("file.java", "class A{ }")
var first = true
val collector = CollectorWithSettings(
object : InlayHintsCollector {
override fun collect(element: PsiElement, editor: Editor, sink: InlayHintsSink): Boolean {
if (first) {
first = false
sink.addInlineElement(0, false, SpacePresentation(1, 1))
}
return true
}
}, noSettings, Language.ANY, InlayHintsSinkImpl(noSettings)
)
collectThenApply(listOf(collector))
fun testNonProviderMangedInlayStayUntouched() {
val presentation = HorizontalConstrainedPresentation(TestRootPresentation(), null)
@Suppress("UNCHECKED_CAST")
inlayModel.addInlineElement(3, InlineInlayRenderer(listOf(presentation as HorizontalConstrainedPresentation<in Any>)))
createPass(emptyList()).collectAndApply()
assertEquals(1, inlineElements.size)
collectThenApply(emptyList())
assertEquals(0, inlineElements.size)
val renderer = inlineElements.first().renderer as InlineInlayRenderer
assertEquals(presentation, renderer.getConstrainedPresentations().first())
}
private fun collectThenApply(collectors: List<CollectorWithSettings<NoSettings>>) {
val pass = createPass(collectors)
pass.doCollectInformation(DumbProgressIndicator())
pass.doApplyInformationToEditor()
fun testInlaysFromMultipleCollectorsMerged() {
createPass(listOf(
createOneOffCollector {
it.addInlineElement(1, TestRootPresentation(1), null)
},
createOneOffCollector {
it.addInlineElement(2, TestRootPresentation(2), null)
}
)).collectAndApply()
val inlays = inlineElements
assertEquals(2, inlays.size)
assertEquals(1, extractContent(inlays, 0))
assertEquals(2, extractContent(inlays, 1))
}
private fun createPass(collectors: List<CollectorWithSettings<NoSettings>>): InlayHintsPass {
return InlayHintsPass(myFixture.file, collectors, myFixture.editor, InlayHintsSettings.instance())
fun testPresentationUpdated() {
createPass(listOf(
createOneOffCollector {
it.addInlineElement(1, TestRootPresentation(1), null)
}
)).collectAndApply()
assertEquals(1, inlineElements.size)
assertEquals(1, extractContent(inlineElements, 0))
createPass(listOf(
createOneOffCollector {
it.addInlineElement(1, TestRootPresentation(3), null)
}
)).collectAndApply()
assertEquals(1, inlineElements.size)
assertEquals(3, extractContent(inlineElements, 0))
}
private val blockElements: MarkList<Inlay<*>>
get() = MarkList(myFixture.editor.inlayModel.getBlockElementsInRange(0, myFixture.file.textRange.endOffset))
fun testNoPresentationRecreatedWhenNothingChanges() {
val initilalRoot = RecursivelyUpdatingRootPresentation(SpacePresentation(10, 10))
applyPassWithCollectorOfSingleElement(initilalRoot)
applyPassWithCollectorOfSingleElement(RecursivelyUpdatingRootPresentation(SpacePresentation(10, 10)))
val root = expectSingleHorizontalPresentation()
assertSame(initilalRoot, root)
}
private val inlineElements: MarkList<Inlay<*>>
get() = MarkList(myFixture.editor.inlayModel.getInlineElementsInRange(0, myFixture.file.textRange.endOffset))
fun testPresentationIsTheSameButContentUpdated() {
val initialContent = SpacePresentation(10, 10)
val initilalRoot = RecursivelyUpdatingRootPresentation(initialContent)
applyPassWithCollectorOfSingleElement(initilalRoot)
applyPassWithCollectorOfSingleElement(RecursivelyUpdatingRootPresentation(SpacePresentation(5, 5)))
val root = expectSingleHorizontalPresentation()
assertSame(initilalRoot, root)
assertNotSame(initialContent, root.content)
}
fun testUpdateWithNonRecursiveRootPresentation() {
applyPassWithCollectorOfSingleElement(RecursivelyUpdatingRootPresentation(SpacePresentation(10, 10)))
val rootAfter = TestRootPresentation(1)
applyPassWithCollectorOfSingleElement(rootAfter)
val presentation = expectSingleHorizontalPresentation()
assertSame(rootAfter, presentation)
}
private fun expectSingleHorizontalPresentation(): RootInlayPresentation<out Any> {
assertEquals(1, inlineElements.size)
val inlay = inlineElements.first()
val renderer = inlay.renderer as InlineInlayRenderer
val presentations = renderer.getConstrainedPresentations()
assertEquals(1, presentations.size)
val constrainedPresentation = presentations.first()
return constrainedPresentation.root
}
private fun <T : Any> applyPassWithCollectorOfSingleElement(presentation: RootInlayPresentation<T>) {
createPass(listOf(
createOneOffCollector {
it.addInlineElement(1, presentation, null)
}
)).collectAndApply()
}
private fun extractContent(inlays: List<Inlay<*>>, index: Int): Int {
val renderer = inlays[index].renderer as InlineInlayRenderer
return renderer.getConstrainedPresentations().first().root.content as Int
}
private fun createOneOffCollector(collector: (InlayHintsSink) -> Unit): CollectorWithSettings<NoSettings> {
var firstTime = true
val collectorLambda: (PsiElement, Editor, InlayHintsSink) -> Boolean = { _, _, sink ->
if (firstTime) {
collector(sink)
firstTime = false
}
false
}
return createCollector(collectorLambda)
}
private fun createCollector(collector: (PsiElement, Editor, InlayHintsSink) -> Boolean): CollectorWithSettings<NoSettings> {
return CollectorWithSettings(object : InlayHintsCollector {
override fun collect(element: PsiElement, editor: Editor, sink: InlayHintsSink): Boolean {
return collector(element, editor, sink)
}
}, noSettings, Language.ANY, InlayHintsSinkImpl(myFixture.editor))
}
private fun createPass(collectors: List<CollectorWithSettings<*>>): InlayHintsPass {
return InlayHintsPass(myFixture.file, collectors, myFixture.editor)
}
private fun InlayHintsPass.collectAndApply() {
val dumbProgressIndicator = DumbProgressIndicator()
doCollectInformation(dumbProgressIndicator)
applyInformationToEditor()
}
private val inlayModel
get() = myFixture.editor.inlayModel
private val blockElements: List<Inlay<*>>
get() = inlayModel.getBlockElementsInRange(0, myFixture.file.textRange.endOffset)
private val inlineElements: List<Inlay<*>>
get() = inlayModel.getInlineElementsInRange(0, myFixture.file.textRange.endOffset)
private val allHintsCount: Int
get() = inlineElements.size + blockElements.size
}

View File

@@ -0,0 +1,125 @@
// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.codeInsight.hints
import com.intellij.codeInsight.hints.presentation.PresentationFactory
import com.intellij.openapi.editor.impl.EditorImpl
import com.intellij.testFramework.LightPlatformCodeInsightTestCase
import junit.framework.TestCase
import java.awt.Rectangle
class InlineInlayRendererTest : LightPlatformCodeInsightTestCase() {
override fun setUp() {
super.setUp()
configureFromFileText("file.java", "class A{ }")
}
fun testHorizontalPresentationPriorityIsHonoredWhenInsert() {
val c1 = constrained(2)
val renderer = InlineInlayRenderer(listOf(c1))
val c2 = constrained(1)
renderer.addOrUpdate(listOf(c2), editor, factory())
val arranged = renderer.getConstrainedPresentations()
assertEquals(listOf(c2, c1), arranged)
}
fun testInitiallyMoreElements() {
val c1 = constrained(3)
val c2 = constrained(4)
val c3 = constrained(5)
val c4 = constrained(6)
val renderer = InlineInlayRenderer(listOf(c1, c2, c3, c4))
val c5 = constrained(2)
val c6 = constrained(1)
renderer.addOrUpdate(listOf(c5, c6), editor, factory())
val arranged = renderer.getConstrainedPresentations()
assertEquals(listOf(c6, c5, c1, c2, c3, c4), arranged)
}
fun testInitiallyLessElements() {
val c1 = constrained(3)
val c2 = constrained(4)
val renderer = InlineInlayRenderer(listOf(c1, c2))
val c3 = constrained(5)
val c4 = constrained(6)
val c5 = constrained(2)
val c6 = constrained(1)
renderer.addOrUpdate(listOf(c3, c4, c5, c6), editor, factory())
val arranged = renderer.getConstrainedPresentations()
assertEquals(listOf(c6, c5, c1, c2, c3, c4), arranged)
}
fun testNonSortedInitially() {
val c1 = constrained(4)
val c2 = constrained(2)
val renderer = InlineInlayRenderer(listOf(c1, c2))
val arranged = renderer.getConstrainedPresentations()
TestCase.assertEquals(listOf(c2, c1), arranged)
}
fun testSamePriorityUpdate() {
val c1 = HorizontalConstrainedPresentation(TestRootPresentation(1), horizontal(1))
val renderer = InlineInlayRenderer(listOf(c1))
val c2 = HorizontalConstrainedPresentation(TestRootPresentation(2), horizontal(1))
renderer.addOrUpdate(listOf(c2), editor, factory())
val presentations = renderer.getConstrainedPresentations()
assertEquals(1, presentations.size)
val updated = presentations.first()
assertEquals(2, updated.root.content)
}
fun testListenerAdded() {
val root = TestRootPresentation(1)
val c1 = HorizontalConstrainedPresentation(root, horizontal(1))
val renderer = InlineInlayRenderer(listOf(c1))
val listener = ChangeCountingListener()
renderer.setListener(listener)
root.fireContentChanged(Rectangle(0,0,0,0))
assertTrue(listener.contentChanged)
}
fun testListenerPreservedAfterUpdate() {
val root = TestRootPresentation(1)
val c1 = HorizontalConstrainedPresentation(root, horizontal(1))
val renderer = InlineInlayRenderer(listOf(c1))
val listener = ChangeCountingListener()
renderer.setListener(listener)
val c2 = HorizontalConstrainedPresentation(TestRootPresentation(2), horizontal(1))
renderer.addOrUpdate(listOf(c2), editor, factory())
root.fireContentChanged(Rectangle(0, 0, 0, 0))
assertEquals(2, listener.contentChangesCount)
}
fun testListenerCalledAfterUpdate() {
val root = TestRootPresentation(1)
val c1 = HorizontalConstrainedPresentation(root, horizontal(1))
val renderer = InlineInlayRenderer(listOf(c1))
val listener = ChangeCountingListener()
renderer.setListener(listener)
val c2 = HorizontalConstrainedPresentation(TestRootPresentation(2), horizontal(1))
renderer.addOrUpdate(listOf(c2), editor, factory())
assertEquals(1, listener.contentChangesCount)
}
private fun factory() = PresentationFactory(editor as EditorImpl)
private fun constrained(priority: Int): HorizontalConstrainedPresentation<Int> {
return HorizontalConstrainedPresentation(TestRootPresentation(priority), horizontal(priority))
}
private fun horizontal(priority: Int) : HorizontalConstraints {
return HorizontalConstraints(priority, true)
}
}

View File

@@ -1,35 +0,0 @@
// Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.codeInsight.hints
import junit.framework.TestCase
internal class MarkListTest : TestCase() {
fun testMark() {
val markList = MarkList(listOf(0, 1, 2, 3))
markList.mark(0)
markList.mark(3)
assertEquals(listOf(1 to 1, 2 to 2), markList.nonUsed())
}
fun testEmpty() {
val markList = MarkList<Int>(listOf())
assertEquals(listOf<Int>(), markList.nonUsed())
}
fun testMarked() {
val markList = MarkList(listOf(1, 2, 3))
assertFalse(markList.marked(0))
markList.mark(0)
assertTrue(markList.marked(0))
}
}
private fun <T> MarkList<T>.nonUsed() : List<Pair<Int, T>> {
val list = mutableListOf<Pair<Int, T>>()
iterateNonMarked { i, item ->
list.add(i to item)
}
return list
}

View File

@@ -5,6 +5,7 @@ import com.intellij.codeInsight.hints.presentation.*
import com.intellij.openapi.editor.impl.EditorImpl
import com.intellij.testFramework.fixtures.BasePlatformTestCase
import junit.framework.TestCase
import java.awt.Color
import java.awt.Point
import java.awt.event.MouseEvent
import javax.swing.JPanel
@@ -100,22 +101,73 @@ class PresentationTest : TestCase() {
assertEquals(1, height)
}
private class ClickCheckingPresentation(
val presentation: InlayPresentation,
var expectedClick: Pair<Int, Int>?
): InlayPresentation by presentation {
fun testContainerPresentationMouseOutside() {
val inner = ClickCheckingPresentation(SpacePresentation(50, 20), expectedClick = null)
val presentation = ContainerInlayPresentation(inner, InlayPresentationFactory.Padding(3, 5, 6, 8), null, Color.RED)
click(presentation, 1, 2)
}
fun testContainerPresentationMouseInside() {
val inner = ClickCheckingPresentation(SpacePresentation(50, 20), expectedClick = 7 to 4)
val presentation = ContainerInlayPresentation(inner, InlayPresentationFactory.Padding(3, 5, 6, 8), null, Color.RED)
click(presentation, 10, 10)
}
override fun mouseClicked(event: MouseEvent, translated: Point) {
val expectedClickVal = expectedClick
if (expectedClickVal == null) {
fail("No clicks expected")
return
fun testContainerPresentationSize() {
val inner = SpacePresentation(50, 20)
val presentation = ContainerInlayPresentation(inner, InlayPresentationFactory.Padding(3, 5, 6, 8), null, Color.RED)
assertEquals(58, presentation.width)
assertEquals(34, presentation.height)
}
fun testVerticalPresentationClick() {
val inner = ClickCheckingPresentation(SpacePresentation(10, 10), 5 to 5)
val presentation = VerticalListInlayPresentation(listOf(
SpacePresentation(20, 10),
inner
))
click(presentation, 5, 15)
inner.assertWasClicked()
}
fun testVerticalPresentationClickInSpareSpace() {
val inner = ClickCheckingPresentation(SpacePresentation(10, 10), null)
val presentation = VerticalListInlayPresentation(listOf(
SpacePresentation(20, 10),
inner
))
click(presentation, 15, 15)
}
fun testVerticalPresentationEvents() {
val inner = SpacePresentation(10, 10)
val presentation = VerticalListInlayPresentation(listOf(
SpacePresentation(20, 10),
inner
))
val listener = ChangeCountingListener()
presentation.addListener(listener)
inner.fireContentChanged()
assertTrue(listener.contentChanged)
}
fun testHoverSeqPresentation() {
var wasHover = false
var hoverFinished = false
val presentation = object: StaticDelegatePresentation(SpacePresentation(10, 10)) {
override fun mouseMoved(event: MouseEvent, translated: Point) {
wasHover = true
}
override fun mouseExited() {
hoverFinished = true
}
assertEquals(expectedClickVal.first, translated.x)
assertEquals(expectedClickVal.second, translated.y)
super.mouseClicked(event, translated)
}
val seqPresentation = SequencePresentation(listOf(presentation))
moveMouse(seqPresentation, 5, 5)
seqPresentation.mouseExited()
assertTrue(wasHover)
assertTrue(hoverFinished)
}
}
@@ -188,6 +240,20 @@ class HeavyPresentationTest : BasePlatformTestCase() {
}
}
fun testRecursiveRootPresentationListener() {
val inner = SpacePresentation(10, 10)
val r1 = RecursivelyUpdatingRootPresentation(inner)
val listener = ChangeCountingListener()
r1.addListener(listener)
val afterUpdateInner = SpacePresentation(20, 20)
r1.update(RecursivelyUpdatingRootPresentation(afterUpdateInner), myFixture.editor, getFactory())
afterUpdateInner.fireContentChanged()
assertEquals(1, listener.contentChangesCount)
}
private fun getFactory() = PresentationFactory(myFixture.editor as EditorImpl)
private fun unwrapFolding(presentation: InlayPresentation): InlayPresentation {
@@ -200,4 +266,9 @@ class HeavyPresentationTest : BasePlatformTestCase() {
private fun click(presentation: InlayPresentation, x: Int, y: Int) {
val event = MouseEvent(JPanel(), 0, 0, 0, x, y, 0, true, 0)
presentation.mouseClicked(event, Point(x, y))
}
private fun moveMouse(presentation: InlayPresentation, x: Int, y: Int) {
val event = MouseEvent(JPanel(), 0, 0, 0, x, y, 0, true, 0)
presentation.mouseMoved(event, Point(x, y))
}

View File

@@ -0,0 +1,100 @@
// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.codeInsight.hints
import com.intellij.codeInsight.hints.presentation.*
import com.intellij.openapi.editor.impl.EditorImpl
import com.intellij.openapi.editor.markup.TextAttributes
import com.intellij.testFramework.LightPlatformCodeInsightTestCase
import com.intellij.testFramework.PlatformTestUtil
import java.awt.Color
import java.awt.Graphics2D
import java.awt.Point
import java.awt.event.MouseEvent
import java.awt.image.BufferedImage
import java.io.File
import javax.imageio.ImageIO
class PresentationUITest : LightPlatformCodeInsightTestCase() {
val factory by lazy { PresentationFactory(editor as EditorImpl) }
override fun setUp() {
super.setUp()
configureFromFileText("test.java", "class A{}")
}
fun getPath() : String {
return PlatformTestUtil.getCommunityPath()
.replace(File.separatorChar, '/') + "/platform/lang-impl/testData/editor/inlays/ui/"
}
fun testContainerPresentation() {
val inner = ContainerInlayPresentation(SpacePresentation(50, 20), null, InlayPresentationFactory.RoundedCorners(10, 10), Color.BLUE)
val presentation = ContainerInlayPresentation(inner, InlayPresentationFactory.Padding(3, 5, 6, 8), null, Color.RED)
testPresentationAsExpected(presentation)
}
fun testVerticalContainerPresentation() {
val vertical = VerticalListInlayPresentation(listOf(
ContainerInlayPresentation(SpacePresentation(30, 10), null, null, Color.BLUE),
ContainerInlayPresentation(SpacePresentation(15, 7), null, null, Color.RED),
ContainerInlayPresentation(SpacePresentation(19, 12), null, null, Color.GREEN)
))
testPresentationAsExpected(vertical)
}
/**
* Send click to presentation
* [x] and [y] must be in coordinates of presentation
*/
private fun emulateClick(presentation: InlayPresentation, x: Int, y: Int) {
presentation.mouseClicked(MouseEvent(null, 0, 0, 0, x, y, 1, false), Point(x, y))
}
private fun testPresentationAsExpected(presentation: InlayPresentation) {
val width = presentation.width
val height = presentation.height
@Suppress("UndesirableClassUsage")
val actual = BufferedImage(width, height, BufferedImage.TYPE_4BYTE_ABGR)
val graphics = actual.graphics
presentation.paint(graphics as Graphics2D, TextAttributes())
val expectedFilePath = getPath() + "/" + getTestName(false) + ".png"
val file = File(expectedFilePath)
if (!REPLACE_WITH_ACTUAL) {
assertTrue(file.exists())
} else {
if (!file.exists()) {
writeImage(actual, file)
fail("No file found, created from actual")
}
}
val expected = ImageIO.read(file)
val imagesEqual = imagesEqual(actual, expected)
if (REPLACE_WITH_ACTUAL) {
writeImage(actual, file)
}
assertTrue("Images are different", imagesEqual)
}
private fun writeImage(actual: BufferedImage, file: File) {
ImageIO.write(actual, "png", file)
}
private fun imagesEqual(img1: BufferedImage, img2: BufferedImage): Boolean {
if (img1.width != img2.width || img1.height != img2.height) return false
for (x in 0 until img1.width) {
for (y in 0 until img1.height) {
if (img1.getRGB(x, y) != img2.getRGB(x, y)) return false
}
}
return true
}
companion object {
var REPLACE_WITH_ACTUAL = false
}
}

View File

@@ -0,0 +1,69 @@
// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.codeInsight.hints
import com.intellij.codeInsight.hints.presentation.BasePresentation
import com.intellij.codeInsight.hints.presentation.InlayPresentation
import com.intellij.codeInsight.hints.presentation.PresentationListener
import com.intellij.codeInsight.hints.presentation.RootInlayPresentation
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.markup.TextAttributes
import org.junit.Assert.*
import java.awt.Dimension
import java.awt.Graphics2D
import java.awt.Point
import java.awt.Rectangle
import java.awt.event.MouseEvent
internal data class TestRootPresentation(override var content: Int = 0) : BasePresentation(), RootInlayPresentation<Int> {
override val key: ContentKey<Int>
get() = InlayKey<Any, Int>("test.root.presentation")
override val width: Int
get() = 2
override val height: Int
get() = 3
override fun update(newPresentationContent: Int, editor: Editor, factory: InlayPresentationFactory): Boolean {
val changed = content != newPresentationContent
this.content = newPresentationContent
return changed
}
override fun paint(g: Graphics2D, attributes: TextAttributes) {
}
}
internal class ClickCheckingPresentation(
val presentation: InlayPresentation,
var expectedClick: Pair<Int, Int>?
): InlayPresentation by presentation {
var wasClicked = false
override fun mouseClicked(event: MouseEvent, translated: Point) {
val expectedClickVal = expectedClick
if (expectedClickVal == null) {
fail("No clicks expected")
return
}
assertEquals(expectedClickVal.first, translated.x)
assertEquals(expectedClickVal.second, translated.y)
wasClicked = true
super.mouseClicked(event, translated)
}
fun assertWasClicked() {
assertTrue(wasClicked)
}
}
internal class ChangeCountingListener : PresentationListener {
var contentChangesCount = 0
val contentChanged: Boolean
get() = contentChangesCount != 0
override fun contentChanged(area: Rectangle) {
contentChangesCount++
}
override fun sizeChanged(previous: Dimension, current: Dimension) {
}
}

View File

@@ -4,6 +4,7 @@ package com.intellij.testFramework.utils.inlays
import com.intellij.codeInsight.hints.CollectorWithSettings
import com.intellij.codeInsight.hints.InlayHintsProvider
import com.intellij.codeInsight.hints.InlayHintsSinkImpl
import com.intellij.codeInsight.hints.LinearOrderInlayRenderer
import com.intellij.codeInsight.hints.presentation.PresentationRenderer
import com.intellij.openapi.editor.Document
import com.intellij.openapi.editor.Inlay
@@ -17,7 +18,7 @@ abstract class InlayHintsProviderTestCase : BasePlatformTestCase() {
val file = myFixture.file!!
val editor = myFixture.editor
val sink = InlayHintsSinkImpl(provider.key)
val sink = InlayHintsSinkImpl(editor)
val collector = provider.getCollectorFor(file, editor, settings, sink) ?: error("Collector is expected")
val collectorWithSettings = CollectorWithSettings(collector, provider.key, file.language, sink)
collectorWithSettings.collectTraversingAndApply(editor, file, true)
@@ -56,13 +57,14 @@ abstract class InlayHintsProviderTestCase : BasePlatformTestCase() {
}
override fun toString(): String {
val renderer = inlay.renderer as? PresentationRenderer ?: error("Only InlayPresentation based inlays supported")
val renderer = inlay.renderer
if (renderer !is PresentationRenderer && renderer !is LinearOrderInlayRenderer<*>) error("renderer not supported")
return buildString {
append("<# ")
if (type == InlayType.Block) {
append("block ")
}
append(renderer.presentation.toString())
append(renderer.toString())
append(" #>")
if (type == InlayType.Block) {
append('\n')