mirror of
https://gitflic.ru/project/openide/openide.git
synced 2025-12-16 14:23:28 +07:00
Inlay hints: introduce updateable roots
GitOrigin-RevId: 7dc566968b76b72fa65d98675da3d830a281a241
This commit is contained in:
committed by
intellij-monorepo-bot
parent
f25202e0b1
commit
53ab9acc89
@@ -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!
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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`() {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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?)
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
}
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 |
@@ -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)))
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
}
|
||||
}
|
||||
@@ -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')
|
||||
|
||||
Reference in New Issue
Block a user