From 53ab9acc8939bf3748cf6e822921b6e51a8c3e60 Mon Sep 17 00:00:00 2001 From: "Roman.Ivanov" Date: Tue, 18 Feb 2020 17:37:27 +0700 Subject: [PATCH] Inlay hints: introduce updateable roots GitOrigin-RevId: 7dc566968b76b72fa65d98675da3d830a281a241 --- .../daemon/impl/JavaLensProvider.java | 3 +- .../daemon/inlays/MethodChainHintsTest.kt | 4 +- .../codeInsight/hints/InlayContraints.kt | 45 ++++ .../codeInsight/hints/InlayHintsProvider.kt | 18 +- .../codeInsight/hints/InlayHintsSink.kt | 16 +- .../hints/InlayPresentationFactory.kt | 76 ++++++ .../hints/PresentationContainerRenderer.kt | 20 ++ .../hints/presentation/InlayPresentation.kt | 5 +- .../RecursivelyUpdatingRootPresentation.kt | 85 +++++++ .../presentation/RootInlayPresentation.kt | 28 +++ .../codeInsight/hints/BlockInlayRenderer.kt | 28 +++ .../codeInsight/hints/HintComponent.kt | 27 --- .../intellij/codeInsight/hints/HintsBuffer.kt | 64 +++++ .../codeInsight/hints/InlayHintsPass.kt | 224 +++++++++++++++--- .../hints/InlayHintsPassFactory.kt | 8 +- .../codeInsight/hints/InlayHintsSinkImpl.kt | 198 +++------------- .../codeInsight/hints/InlayHintsUtils.kt | 128 ++++++++-- .../codeInsight/hints/InlineInlayRenderer.kt | 27 +++ .../hints/LinearOrderInlayRenderer.kt | 113 +++++++++ .../intellij/codeInsight/hints/MarkList.kt | 36 --- .../ContainerInlayPresentation.kt | 109 +++++++++ .../presentation/InlayEditorListeners.kt | 53 +++++ .../hints/presentation/InsetPresentation.kt | 25 +- .../presentation/MouseHandlingPresentation.kt | 32 +++ .../hints/presentation/OnHoverPresentation.kt | 10 +- .../hints/presentation/PresentationFactory.kt | 84 +++---- .../presentation/PresentationRenderer.kt | 16 +- .../presentation/SequencePresentation.kt | 32 ++- .../hints/presentation/SpacePresentation.kt | 5 + .../VerticalListInlayPresentation.kt | 131 ++++++++++ .../presentation/inlayEditorListeners.kt | 55 ----- .../inlays/ui/ContainerPresentation.png | Bin 0 -> 227 bytes .../ui/VerticalContainerPresentation.png | Bin 0 -> 186 bytes .../codeInsight/hints/BulkEstimateTest.kt | 47 ++++ .../codeInsight/hints/InlayPassTest.kt | 185 +++++++++++---- .../hints/InlineInlayRendererTest.kt | 125 ++++++++++ .../codeInsight/hints/MarkListTest.kt | 35 --- .../codeInsight/hints/PresentationTest.kt | 95 +++++++- .../codeInsight/hints/PresentationUITest.kt | 100 ++++++++ .../codeInsight/hints/TestInlayUtils.kt | 69 ++++++ .../inlays/InlayHintsProviderTestCase.kt | 8 +- 41 files changed, 1827 insertions(+), 542 deletions(-) create mode 100644 platform/lang-api/src/com/intellij/codeInsight/hints/InlayContraints.kt create mode 100644 platform/lang-api/src/com/intellij/codeInsight/hints/InlayPresentationFactory.kt create mode 100644 platform/lang-api/src/com/intellij/codeInsight/hints/PresentationContainerRenderer.kt create mode 100644 platform/lang-api/src/com/intellij/codeInsight/hints/presentation/RecursivelyUpdatingRootPresentation.kt create mode 100644 platform/lang-api/src/com/intellij/codeInsight/hints/presentation/RootInlayPresentation.kt create mode 100644 platform/lang-impl/src/com/intellij/codeInsight/hints/BlockInlayRenderer.kt delete mode 100644 platform/lang-impl/src/com/intellij/codeInsight/hints/HintComponent.kt create mode 100644 platform/lang-impl/src/com/intellij/codeInsight/hints/HintsBuffer.kt create mode 100644 platform/lang-impl/src/com/intellij/codeInsight/hints/InlineInlayRenderer.kt create mode 100644 platform/lang-impl/src/com/intellij/codeInsight/hints/LinearOrderInlayRenderer.kt delete mode 100644 platform/lang-impl/src/com/intellij/codeInsight/hints/MarkList.kt create mode 100644 platform/lang-impl/src/com/intellij/codeInsight/hints/presentation/ContainerInlayPresentation.kt create mode 100644 platform/lang-impl/src/com/intellij/codeInsight/hints/presentation/InlayEditorListeners.kt create mode 100644 platform/lang-impl/src/com/intellij/codeInsight/hints/presentation/MouseHandlingPresentation.kt create mode 100644 platform/lang-impl/src/com/intellij/codeInsight/hints/presentation/VerticalListInlayPresentation.kt delete mode 100644 platform/lang-impl/src/com/intellij/codeInsight/hints/presentation/inlayEditorListeners.kt create mode 100644 platform/lang-impl/testData/editor/inlays/ui/ContainerPresentation.png create mode 100644 platform/lang-impl/testData/editor/inlays/ui/VerticalContainerPresentation.png create mode 100644 platform/lang-impl/testSources/com/intellij/codeInsight/hints/BulkEstimateTest.kt create mode 100644 platform/lang-impl/testSources/com/intellij/codeInsight/hints/InlineInlayRendererTest.kt delete mode 100644 platform/lang-impl/testSources/com/intellij/codeInsight/hints/MarkListTest.kt create mode 100644 platform/lang-impl/testSources/com/intellij/codeInsight/hints/PresentationUITest.kt create mode 100644 platform/lang-impl/testSources/com/intellij/codeInsight/hints/TestInlayUtils.kt diff --git a/java/java-impl/src/com/intellij/codeInsight/daemon/impl/JavaLensProvider.java b/java/java-impl/src/com/intellij/codeInsight/daemon/impl/JavaLensProvider.java index ee1eb2fc3011..500e38643437 100644 --- a/java/java-impl/src/com/intellij/codeInsight/daemon/impl/JavaLensProvider.java +++ b/java/java-impl/src/com/intellij/codeInsight/daemon/impl/JavaLensProvider.java @@ -283,6 +283,7 @@ public class JavaLensProvider implements InlayHintsProvider, 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! } } diff --git a/java/java-tests/testSrc/com/intellij/java/codeInsight/daemon/inlays/MethodChainHintsTest.kt b/java/java-tests/testSrc/com/intellij/java/codeInsight/daemon/inlays/MethodChainHintsTest.kt index 4f900c13adf1..aa570fa664a5 100644 --- a/java/java-tests/testSrc/com/intellij/java/codeInsight/daemon/inlays/MethodChainHintsTest.kt +++ b/java/java-tests/testSrc/com/intellij/java/codeInsight/daemon/inlays/MethodChainHintsTest.kt @@ -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`() { diff --git a/platform/lang-api/src/com/intellij/codeInsight/hints/InlayContraints.kt b/platform/lang-api/src/com/intellij/codeInsight/hints/InlayContraints.kt new file mode 100644 index 000000000000..eaa1fec8e63e --- /dev/null +++ b/platform/lang-api/src/com/intellij/codeInsight/hints/InlayContraints.kt @@ -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 { + val root: RootInlayPresentation + + /** + * 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( + override val root: RootInlayPresentation, + override val constraints: HorizontalConstraints? +) : ConstrainedPresentation { + override val priority: Int + get() = constraints?.priority ?: 0 +} + +class BlockConstraints( + val relatesToPrecedingText: Boolean, + val priority: Int +) + + +data class BlockConstrainedPresentation( + override val root: RootInlayPresentation, + override val constraints: BlockConstraints? +) : ConstrainedPresentation{ + override val priority: Int + get() = constraints?.priority ?: 0 +} \ No newline at end of file diff --git a/platform/lang-api/src/com/intellij/codeInsight/hints/InlayHintsProvider.kt b/platform/lang-api/src/com/intellij/codeInsight/hints/InlayHintsProvider.kt index 45113ebf3391..fbebcacdbbc5 100644 --- a/platform/lang-api/src/com/intellij/codeInsight/hints/InlayHintsProvider.kt +++ b/platform/lang-api/src/com/intellij/codeInsight/hints/InlayHintsProvider.kt @@ -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>(EX * * To test it you may use InlayHintsProviderTestCase. */ -@ApiStatus.Experimental interface InlayHintsProvider { /** * 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(val id: String) { fun getFullId(language: Language): String = language.id + "." + id -} \ No newline at end of file +} + +interface AbstractSettingsKey { + fun getFullId(language: Language): String +} + +data class InlayKey(val id: String) : AbstractSettingsKey, ContentKey { + override fun getFullId(language: Language): String = language.id + "." + id +} + +/** + * Allows type-safe access to content of the root presentation + */ +interface ContentKey \ No newline at end of file diff --git a/platform/lang-api/src/com/intellij/codeInsight/hints/InlayHintsSink.kt b/platform/lang-api/src/com/intellij/codeInsight/hints/InlayHintsSink.kt index 6b83465a44c8..d140b6a593d8 100644 --- a/platform/lang-api/src/com/intellij/codeInsight/hints/InlayHintsSink.kt +++ b/platform/lang-api/src/com/intellij/codeInsight/hints/InlayHintsSink.kt @@ -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?) } \ No newline at end of file diff --git a/platform/lang-api/src/com/intellij/codeInsight/hints/InlayPresentationFactory.kt b/platform/lang-api/src/com/intellij/codeInsight/hints/InlayPresentationFactory.kt new file mode 100644 index 000000000000..a3dedf3734a5 --- /dev/null +++ b/platform/lang-api/src/com/intellij/codeInsight/hints/InlayPresentationFactory.kt @@ -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 + ) +} diff --git a/platform/lang-api/src/com/intellij/codeInsight/hints/PresentationContainerRenderer.kt b/platform/lang-api/src/com/intellij/codeInsight/hints/PresentationContainerRenderer.kt new file mode 100644 index 000000000000..fc0d983ae450 --- /dev/null +++ b/platform/lang-api/src/com/intellij/codeInsight/hints/PresentationContainerRenderer.kt @@ -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 : EditorCustomElementRenderer, InputHandler { + fun setListener(listener: PresentationListener) + + fun addOrUpdate(new: List>, editor: Editor, factory: InlayPresentationFactory) + + fun isAcceptablePlacement(placement: Inlay.Placement): Boolean +} \ No newline at end of file diff --git a/platform/lang-api/src/com/intellij/codeInsight/hints/presentation/InlayPresentation.kt b/platform/lang-api/src/com/intellij/codeInsight/hints/presentation/InlayPresentation.kt index 3fa48916de6b..39df1ec69a7d 100644 --- a/platform/lang-api/src/com/intellij/codeInsight/hints/presentation/InlayPresentation.kt +++ b/platform/lang-api/src/com/intellij/codeInsight/hints/presentation/InlayPresentation.kt @@ -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 diff --git a/platform/lang-api/src/com/intellij/codeInsight/hints/presentation/RecursivelyUpdatingRootPresentation.kt b/platform/lang-api/src/com/intellij/codeInsight/hints/presentation/RecursivelyUpdatingRootPresentation.kt new file mode 100644 index 000000000000..43bc35a7050b --- /dev/null +++ b/platform/lang-api/src/com/intellij/codeInsight/hints/presentation/RecursivelyUpdatingRootPresentation.kt @@ -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 { + 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 + 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 = InlayKey("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) + } + } +} \ No newline at end of file diff --git a/platform/lang-api/src/com/intellij/codeInsight/hints/presentation/RootInlayPresentation.kt b/platform/lang-api/src/com/intellij/codeInsight/hints/presentation/RootInlayPresentation.kt new file mode 100644 index 000000000000..99007a48e34f --- /dev/null +++ b/platform/lang-api/src/com/intellij/codeInsight/hints/presentation/RootInlayPresentation.kt @@ -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 : 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 +} \ No newline at end of file diff --git a/platform/lang-impl/src/com/intellij/codeInsight/hints/BlockInlayRenderer.kt b/platform/lang-impl/src/com/intellij/codeInsight/hints/BlockInlayRenderer.kt new file mode 100644 index 000000000000..b8e08156295c --- /dev/null +++ b/platform/lang-impl/src/com/intellij/codeInsight/hints/BlockInlayRenderer.kt @@ -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> +) : LinearOrderInlayRenderer( + 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 } + } +} \ No newline at end of file diff --git a/platform/lang-impl/src/com/intellij/codeInsight/hints/HintComponent.kt b/platform/lang-impl/src/com/intellij/codeInsight/hints/HintComponent.kt deleted file mode 100644 index 1cf2baa344fe..000000000000 --- a/platform/lang-impl/src/com/intellij/codeInsight/hints/HintComponent.kt +++ /dev/null @@ -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( - 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) - } -} \ No newline at end of file diff --git a/platform/lang-impl/src/com/intellij/codeInsight/hints/HintsBuffer.kt b/platform/lang-impl/src/com/intellij/codeInsight/hints/HintsBuffer.kt new file mode 100644 index 000000000000..f5352bc6de45 --- /dev/null +++ b/platform/lang-impl/src/com/intellij/codeInsight/hints/HintsBuffer.kt @@ -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>>() + val blockBelowHints = TIntObjectHashMap>>() + val blockAboveHints = TIntObjectHashMap>>() + + 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>? { + return getMap(placement).remove(offset) + } + + @Suppress("UNCHECKED_CAST") + private fun getMap(placement: Inlay.Placement) : TIntObjectHashMap>> { + 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>> + } +} + +fun TIntObjectHashMap>.mergeIntoThis(another: TIntObjectHashMap>) { + another.forEachEntry { otherOffset, otherList -> + val current = this[otherOffset] + if (current == null) { + put(otherOffset, otherList) + } else { + current.addAll(otherList) + } + true + } +} \ No newline at end of file diff --git a/platform/lang-impl/src/com/intellij/codeInsight/hints/InlayHintsPass.kt b/platform/lang-impl/src/com/intellij/codeInsight/hints/InlayHintsPass.kt index c837f13f683d..2fc5d8340f1b 100644 --- a/platform/lang-impl/src/com/intellij/codeInsight/hints/InlayHintsPass.kt +++ b/platform/lang-impl/src/com/intellij/codeInsight/hints/InlayHintsPass.kt @@ -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>, - editor: Editor, - val settings: InlayHintsSettings + private val rootElement: PsiElement, + private val enabledCollectors: List>, + 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() 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> = MarkList(inlayModel.getInlineElementsInRange(startOffset, endOffset)) - val existingVerticalInlays: MarkList> = 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>) { - // 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>) : 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("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> = inlayModel.getBlockElementsInRange(startOffset, endOffset, + BlockInlayRenderer::class.java) + val existingBlockAboveInlays = mutableListOf>>() + val existingBlockBelowInlays = mutableListOf>>() + 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>) { + 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>>, + 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>, + existingBlockAboveInlays: MutableList>>, + existingBlockBelowInlays: MutableList>>): 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>.offsets(): IntStream = stream().mapToInt { it.offset } + + private fun updateOrDispose(existing: List>>, + 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 PresentationContainerRenderer.addOrUpdate( + new: List>, + factory: InlayPresentationFactory, + placement: Inlay.Placement, + editor: Editor + ) { + if (!isAcceptablePlacement(placement)) { + throw IllegalArgumentException() + } + @Suppress("UNCHECKED_CAST") + return addOrUpdate(new as List>, editor, factory) + } + } +} \ No newline at end of file diff --git a/platform/lang-impl/src/com/intellij/codeInsight/hints/InlayHintsPassFactory.kt b/platform/lang-impl/src/com/intellij/codeInsight/hints/InlayHintsPassFactory.kt index 24a2236377aa..3998c7277620 100644 --- a/platform/lang-impl/src/com/intellij/codeInsight/hints/InlayHintsPassFactory.kt +++ b/platform/lang-impl/src/com/intellij/codeInsight/hints/InlayHintsPassFactory.kt @@ -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("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("inlay.psi.modification.stamp") + fun putCurrentModificationStamp(editor: Editor, file: PsiFile) { editor.putUserData(PSI_MODIFICATION_STAMP, getCurrentModificationStamp(file)) } diff --git a/platform/lang-impl/src/com/intellij/codeInsight/hints/InlayHintsSinkImpl.kt b/platform/lang-impl/src/com/intellij/codeInsight/hints/InlayHintsSinkImpl.kt index 3d4f81060b16..9386e9dc17e9 100644 --- a/platform/lang-impl/src/com/intellij/codeInsight/hints/InlayHintsSinkImpl.kt +++ b/platform/lang-impl/src/com/intellij/codeInsight/hints/InlayHintsSinkImpl.kt @@ -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(val key: SettingsKey) : InlayHintsSink { - private val hints = TIntObjectHashMap() +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(val key: SettingsKey) : 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>, - existingVerticalInlays: MarkList>, - 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? { - 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) : 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>, - existingVerticalInlays: MarkList>, - isEnabled: Boolean - ) { - updateOrDeleteExistingHints(existingHorizontalInlays, true, isEnabled) - updateOrDeleteExistingHints(existingVerticalInlays, false, isEnabled) - } - - private fun updateOrDeleteExistingHints(existingInlays: MarkList>, 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)) - 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> = Key.create("INLAY_KEY") - private const val BulkChangeThreshold = 1000 - - @JvmField val LOG = logger>() + private fun TIntObjectHashMap>>.addCreatingListIfNeeded( + offset: Int, + value: ConstrainedPresentation<*, T> + ) { + var list = this[offset] + if (list == null) { + list = SmartList() + put(offset, list) + } + list.add(value) + } } } \ No newline at end of file diff --git a/platform/lang-impl/src/com/intellij/codeInsight/hints/InlayHintsUtils.kt b/platform/lang-impl/src/com/intellij/codeInsight/hints/InlayHintsUtils.kt index 11b6d3557fb1..bd39f8f14f6a 100644 --- a/platform/lang-impl/src/com/intellij/codeInsight/hints/InlayHintsUtils.kt +++ b/platform/lang-impl/src/com/intellij/codeInsight/hints/InlayHintsUtils.kt @@ -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 ProviderWithSettings.withSettingsCopy(): ProviderWithSettings ProviderWithSettings.getCollectorWrapperFor(file: PsiFile, editor: Editor, language: Language): CollectorWithSettings? { 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( val collector: InlayHintsCollector, val key: SettingsKey, val language: Language, - val sink: InlayHintsSinkImpl + val sink: InlayHintsSinkImpl ) { fun collectHints(element: PsiElement, editor: Editor): Boolean { return collector.collect(element, editor, sink) } - fun applyToEditor( - editor: Editor, - existingHorizontalInlays: MarkList>, - existingVerticalInlays: MarkList>, - 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> = MarkList(model.getInlineElementsInRange(startOffset, endOffset)) - val existingVerticalInlays: MarkList> = 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) \ No newline at end of file +fun InlayPresentation.dimension() = Dimension(width, height) + +private typealias ConstrPresent = 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 produceUpdatedRootList( + new: List>, + old: List>, + editor: Editor, + factory: InlayPresentationFactory + ): List> { + val updatedPresentations: MutableList> = 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 RootInlayPresentation.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) + } +} \ No newline at end of file diff --git a/platform/lang-impl/src/com/intellij/codeInsight/hints/InlineInlayRenderer.kt b/platform/lang-impl/src/com/intellij/codeInsight/hints/InlineInlayRenderer.kt new file mode 100644 index 000000000000..4a4a3d5b0d99 --- /dev/null +++ b/platform/lang-impl/src/com/intellij/codeInsight/hints/InlineInlayRenderer.kt @@ -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> +) : LinearOrderInlayRenderer( + 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 } + } +} + diff --git a/platform/lang-impl/src/com/intellij/codeInsight/hints/LinearOrderInlayRenderer.kt b/platform/lang-impl/src/com/intellij/codeInsight/hints/LinearOrderInlayRenderer.kt new file mode 100644 index 000000000000..2e753f3acdf5 --- /dev/null +++ b/platform/lang-impl/src/com/intellij/codeInsight/hints/LinearOrderInlayRenderer.kt @@ -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( + constrainedPresentations: Collection>, + private val createPresentation: (List>) -> InlayPresentation, + private val comparator: (ConstrainedPresentation<*, Constraint>) -> Int +) : PresentationContainerRenderer { + // Supposed to be changed rarely and rarely contains more than 1 element + private var presentations: List> = SmartList(constrainedPresentations.sortedBy(comparator)) + + init { + assert(presentations.isNotEmpty()) + } + + private var cachedPresentation = createPresentation(presentations) + + private var _listener: PresentationListener? = null + + override fun addOrUpdate(new: List>, 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>, + 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> = presentations + + companion object { + fun effectsIn(attributes: TextAttributes): TextAttributes { + val result = TextAttributes() + result.effectType = attributes.effectType + result.effectColor = attributes.effectColor + return result + } + } +} \ No newline at end of file diff --git a/platform/lang-impl/src/com/intellij/codeInsight/hints/MarkList.kt b/platform/lang-impl/src/com/intellij/codeInsight/hints/MarkList.kt deleted file mode 100644 index 63faa750b24c..000000000000 --- a/platform/lang-impl/src/com/intellij/codeInsight/hints/MarkList.kt +++ /dev/null @@ -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(private val items: List) : Iterable { - private val myMarked = BitSet(items.size) - - operator fun get(index: Int) : T = items[index] - - val size: Int - get() = items.size - - override fun iterator(): Iterator = 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) - } - } -} \ No newline at end of file diff --git a/platform/lang-impl/src/com/intellij/codeInsight/hints/presentation/ContainerInlayPresentation.kt b/platform/lang-impl/src/com/intellij/codeInsight/hints/presentation/ContainerInlayPresentation.kt new file mode 100644 index 000000000000..e28ad56a2a0c --- /dev/null +++ b/platform/lang-impl/src/com/intellij/codeInsight/hints/presentation/ContainerInlayPresentation.kt @@ -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() + } + +} \ No newline at end of file diff --git a/platform/lang-impl/src/com/intellij/codeInsight/hints/presentation/InlayEditorListeners.kt b/platform/lang-impl/src/com/intellij/codeInsight/hints/presentation/InlayEditorListeners.kt new file mode 100644 index 000000000000..121bb3c9e848 --- /dev/null +++ b/platform/lang-impl/src/com/intellij/codeInsight/hints/presentation/InlayEditorListeners.kt @@ -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) + } +} \ No newline at end of file diff --git a/platform/lang-impl/src/com/intellij/codeInsight/hints/presentation/InsetPresentation.kt b/platform/lang-impl/src/com/intellij/codeInsight/hints/presentation/InsetPresentation.kt index df93e52dcba9..4112fdd3e3f2 100644 --- a/platform/lang-impl/src/com/intellij/codeInsight/hints/presentation/InsetPresentation.kt +++ b/platform/lang-impl/src/com/intellij/codeInsight/hints/presentation/InsetPresentation.kt @@ -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 } } \ No newline at end of file diff --git a/platform/lang-impl/src/com/intellij/codeInsight/hints/presentation/MouseHandlingPresentation.kt b/platform/lang-impl/src/com/intellij/codeInsight/hints/presentation/MouseHandlingPresentation.kt new file mode 100644 index 000000000000..8c14144486b0 --- /dev/null +++ b/platform/lang-impl/src/com/intellij/codeInsight/hints/presentation/MouseHandlingPresentation.kt @@ -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() + } +} \ No newline at end of file diff --git a/platform/lang-impl/src/com/intellij/codeInsight/hints/presentation/OnHoverPresentation.kt b/platform/lang-impl/src/com/intellij/codeInsight/hints/presentation/OnHoverPresentation.kt index 66f3339e5a4a..31078174cbc0 100644 --- a/platform/lang-impl/src/com/intellij/codeInsight/hints/presentation/OnHoverPresentation.kt +++ b/platform/lang-impl/src/com/intellij/codeInsight/hints/presentation/OnHoverPresentation.kt @@ -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() } } \ No newline at end of file diff --git a/platform/lang-impl/src/com/intellij/codeInsight/hints/presentation/PresentationFactory.kt b/platform/lang-impl/src/com/intellij/codeInsight/hints/presentation/PresentationFactory.kt index 7ace68143597..56c16aab42ca 100644 --- a/platform/lang-impl/src/com/intellij/codeInsight/hints/presentation/PresentationFactory.kt +++ b/platform/lang-impl/src/com/intellij/codeInsight/hints/presentation/PresentationFactory.kt @@ -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 { val (leftMatching, rightMatching) = matching(listOf(left, right)) @@ -188,8 +184,8 @@ class PresentationFactory(private val editor: EditorImpl) { decorator: (InlayPresentation) -> InlayPresentation): List { 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 { diff --git a/platform/lang-impl/src/com/intellij/codeInsight/hints/presentation/PresentationRenderer.kt b/platform/lang-impl/src/com/intellij/codeInsight/hints/presentation/PresentationRenderer.kt index 371e50338d82..65edd4ba9e94 100644 --- a/platform/lang-impl/src/com/intellij/codeInsight/hints/presentation/PresentationRenderer.kt +++ b/platform/lang-impl/src/com/intellij/codeInsight/hints/presentation/PresentationRenderer.kt @@ -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() + } } \ No newline at end of file diff --git a/platform/lang-impl/src/com/intellij/codeInsight/hints/presentation/SequencePresentation.kt b/platform/lang-impl/src/com/intellij/codeInsight/hints/presentation/SequencePresentation.kt index ac583728b13d..8aa90b16fb0b 100644 --- a/platform/lang-impl/src/com/intellij/codeInsight/hints/presentation/SequencePresentation.kt +++ b/platform/lang-impl/src/com/intellij/codeInsight/hints/presentation/SequencePresentation.kt @@ -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) : BasePresentation() { init { if (presentations.isEmpty()) throw IllegalArgumentException() @@ -23,7 +28,9 @@ class SequencePresentation(val presentations: List) : 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) : 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) : 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) : 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) diff --git a/platform/lang-impl/src/com/intellij/codeInsight/hints/presentation/SpacePresentation.kt b/platform/lang-impl/src/com/intellij/codeInsight/hints/presentation/SpacePresentation.kt index 20cca3c99c92..e3b80e69e1b1 100644 --- a/platform/lang-impl/src/com/intellij/codeInsight/hints/presentation/SpacePresentation.kt +++ b/platform/lang-impl/src/com/intellij/codeInsight/hints/presentation/SpacePresentation.kt @@ -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 + } } \ No newline at end of file diff --git a/platform/lang-impl/src/com/intellij/codeInsight/hints/presentation/VerticalListInlayPresentation.kt b/platform/lang-impl/src/com/intellij/codeInsight/hints/presentation/VerticalListInlayPresentation.kt new file mode 100644 index 000000000000..91f42e0fed2d --- /dev/null +++ b/platform/lang-impl/src/com/intellij/codeInsight/hints/presentation/VerticalListInlayPresentation.kt @@ -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 +) : 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() + } + } +} \ No newline at end of file diff --git a/platform/lang-impl/src/com/intellij/codeInsight/hints/presentation/inlayEditorListeners.kt b/platform/lang-impl/src/com/intellij/codeInsight/hints/presentation/inlayEditorListeners.kt deleted file mode 100644 index 207181b3b1da..000000000000 --- a/platform/lang-impl/src/com/intellij/codeInsight/hints/presentation/inlayEditorListeners.kt +++ /dev/null @@ -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) - } - } - } -} \ No newline at end of file diff --git a/platform/lang-impl/testData/editor/inlays/ui/ContainerPresentation.png b/platform/lang-impl/testData/editor/inlays/ui/ContainerPresentation.png new file mode 100644 index 0000000000000000000000000000000000000000..2dcca49ec97c7453ee9932e065faddb76505b0d8 GIT binary patch literal 227 zcmeAS@N?(olHy`uVBq!ia0vp^RzR%8!3HGx>`vVSQmZ^&978JN-rm^Fd&ohc<>AV9 zzZWfYdVml}SBd#LJg{We+-!YteuP!Fz5SBSfeyYLRSU8k%d{`#y>*&zaYc6dt_M-+ zcm7$OI}vHQ*=nDYyoH{6ap&gM>EfqWFY3Pgt0dh#z>aCx1zjnToer}1PR3tcaDsJJ zcT8}~f_Ium0pCR=S5$ZLT(NaM*V=cJsqf~M{+~X{%65D1_x_lAX{*C-(Twd(Z?8$b Z;9l$=S>+HXpAK|2gQu&X%Q~loCIE07UJw8P literal 0 HcmV?d00001 diff --git a/platform/lang-impl/testData/editor/inlays/ui/VerticalContainerPresentation.png b/platform/lang-impl/testData/editor/inlays/ui/VerticalContainerPresentation.png new file mode 100644 index 0000000000000000000000000000000000000000..addeefc4ab45e6d32e6bbb42c874249d246eb3b5 GIT binary patch literal 186 zcmeAS@N?(olHy`uVBq!ia0vp^azHH0!3HGvF1f=4q#8Y4978JNj!rh@Y6uWH{O_;+ z+8xWf7X}4yvdP$9es1Tw`kuq5EY@HUC-&_5Zc=dOgf8pxo;yJpj3OKPuc0D3=7AgT kxEw4JW("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))) + 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>) { - 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>): 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> - 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> - 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 { + 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 applyPassWithCollectorOfSingleElement(presentation: RootInlayPresentation) { + createPass(listOf( + createOneOffCollector { + it.addInlineElement(1, presentation, null) + } + )).collectAndApply() + } + + private fun extractContent(inlays: List>, 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 { + 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 { + 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>): 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> + get() = inlayModel.getBlockElementsInRange(0, myFixture.file.textRange.endOffset) + + private val inlineElements: List> + get() = inlayModel.getInlineElementsInRange(0, myFixture.file.textRange.endOffset) + + private val allHintsCount: Int + get() = inlineElements.size + blockElements.size } \ No newline at end of file diff --git a/platform/lang-impl/testSources/com/intellij/codeInsight/hints/InlineInlayRendererTest.kt b/platform/lang-impl/testSources/com/intellij/codeInsight/hints/InlineInlayRendererTest.kt new file mode 100644 index 000000000000..ef0ac1c8b8be --- /dev/null +++ b/platform/lang-impl/testSources/com/intellij/codeInsight/hints/InlineInlayRendererTest.kt @@ -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 { + return HorizontalConstrainedPresentation(TestRootPresentation(priority), horizontal(priority)) + } + + private fun horizontal(priority: Int) : HorizontalConstraints { + return HorizontalConstraints(priority, true) + } +} \ No newline at end of file diff --git a/platform/lang-impl/testSources/com/intellij/codeInsight/hints/MarkListTest.kt b/platform/lang-impl/testSources/com/intellij/codeInsight/hints/MarkListTest.kt deleted file mode 100644 index 0a667cd9322a..000000000000 --- a/platform/lang-impl/testSources/com/intellij/codeInsight/hints/MarkListTest.kt +++ /dev/null @@ -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(listOf()) - assertEquals(listOf(), markList.nonUsed()) - } - - fun testMarked() { - val markList = MarkList(listOf(1, 2, 3)) - assertFalse(markList.marked(0)) - markList.mark(0) - assertTrue(markList.marked(0)) - } -} - -private fun MarkList.nonUsed() : List> { - val list = mutableListOf>() - iterateNonMarked { i, item -> - list.add(i to item) - } - return list -} \ No newline at end of file diff --git a/platform/lang-impl/testSources/com/intellij/codeInsight/hints/PresentationTest.kt b/platform/lang-impl/testSources/com/intellij/codeInsight/hints/PresentationTest.kt index 33fd60c32e2a..0a756a454b8d 100644 --- a/platform/lang-impl/testSources/com/intellij/codeInsight/hints/PresentationTest.kt +++ b/platform/lang-impl/testSources/com/intellij/codeInsight/hints/PresentationTest.kt @@ -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? - ): 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)) } \ No newline at end of file diff --git a/platform/lang-impl/testSources/com/intellij/codeInsight/hints/PresentationUITest.kt b/platform/lang-impl/testSources/com/intellij/codeInsight/hints/PresentationUITest.kt new file mode 100644 index 000000000000..0daadccd0d84 --- /dev/null +++ b/platform/lang-impl/testSources/com/intellij/codeInsight/hints/PresentationUITest.kt @@ -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 + } +} \ No newline at end of file diff --git a/platform/lang-impl/testSources/com/intellij/codeInsight/hints/TestInlayUtils.kt b/platform/lang-impl/testSources/com/intellij/codeInsight/hints/TestInlayUtils.kt new file mode 100644 index 000000000000..f141087270d3 --- /dev/null +++ b/platform/lang-impl/testSources/com/intellij/codeInsight/hints/TestInlayUtils.kt @@ -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 { + override val key: ContentKey + get() = InlayKey("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? +): 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) { + } +} \ No newline at end of file diff --git a/platform/testFramework/src/com/intellij/testFramework/utils/inlays/InlayHintsProviderTestCase.kt b/platform/testFramework/src/com/intellij/testFramework/utils/inlays/InlayHintsProviderTestCase.kt index c09fffa88a6c..3a50a09f819c 100644 --- a/platform/testFramework/src/com/intellij/testFramework/utils/inlays/InlayHintsProviderTestCase.kt +++ b/platform/testFramework/src/com/intellij/testFramework/utils/inlays/InlayHintsProviderTestCase.kt @@ -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')