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 2509dea5b02c..b498b4105f10 100644 --- a/platform/lang-api/src/com/intellij/codeInsight/hints/InlayHintsSink.kt +++ b/platform/lang-api/src/com/intellij/codeInsight/hints/InlayHintsSink.kt @@ -6,14 +6,14 @@ import com.intellij.codeInsight.hints.presentation.InlayPresentation interface InlayHintsSink { /** * Adds inline element to underlying editor. - * Note, that single provider may add only one presentation to the given offset (no matter inline or block). This requirement may be relaxed in future. + * 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) /** * Adds block element to underlying editor. - * Note, that single provider may add only one presentation to the given offset (no matter inline or block). This requirement may be relaxed in future. + * 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) 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 2a196dceba2b..ded942881db0 100644 --- a/platform/lang-impl/src/com/intellij/codeInsight/hints/InlayHintsSinkImpl.kt +++ b/platform/lang-impl/src/com/intellij/codeInsight/hints/InlayHintsSinkImpl.kt @@ -4,6 +4,7 @@ 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.openapi.editor.Editor import com.intellij.openapi.editor.EditorCustomElementRenderer import com.intellij.openapi.editor.Inlay @@ -32,8 +33,10 @@ private class BlockElement( presentation: InlayPresentation ) : InlayHint(offset, presentation) +private class HintsAtOffset(var inlineElement: InlineElement?, var blockElement: BlockElement?) + class InlayHintsSinkImpl(val key: SettingsKey) : InlayHintsSink { - private val hints = TIntObjectHashMap() + private val hints = TIntObjectHashMap() override fun addInlineElement(offset: Int, relatesToPrecedingText: Boolean, presentation: InlayPresentation) { addHint(InlineElement(offset, relatesToPrecedingText, presentation)) @@ -48,10 +51,37 @@ class InlayHintsSinkImpl(val key: SettingsKey) : InlayHintsSink { } private fun addHint(hint: InlayHint) { - hints.put(hint.offset, hint) + 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) + } + } + } + } + + 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: List>, existingVerticalInlays: List>, @@ -66,25 +96,35 @@ class InlayHintsSinkImpl(val key: SettingsKey) : InlayHintsSink { } private fun createNewHints(inlayModel: InlayModel) { - hints.forEachEntry { offset, hint -> - 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@forEachEntry true - inlay.putUserData(INLAY_KEY, key) - presentation.addListener(InlayListener(inlay)) + 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 + inlay.putUserData(INLAY_KEY, 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() @@ -106,13 +146,18 @@ class InlayHintsSinkImpl(val key: SettingsKey) : InlayHintsSink { val inlayKey = inlay.getUserData(INLAY_KEY) as SettingsKey<*>? if (inlayKey != key) continue val offset = inlay.offset - val newHint = hints[offset] - if (newHint != null && newHint is InlineElement != isInline) continue - if (newHint == null || !isEnabled) { + 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 = newHint.presentation + val newPresentation = hint.presentation val renderer = inlay.renderer as PresentationRenderer val previousPresentation = renderer.presentation @Suppress("UNCHECKED_CAST") @@ -129,5 +174,7 @@ class InlayHintsSinkImpl(val key: SettingsKey) : InlayHintsSink { companion object { private val INLAY_KEY: Key = Key.create("INLAY_KEY") private const val BulkChangeThreshold = 1000 + + @JvmField val LOG = logger>() } } \ No newline at end of file diff --git a/platform/lang-impl/testSources/com/intellij/codeInsight/hints/InlayPassTest.kt b/platform/lang-impl/testSources/com/intellij/codeInsight/hints/InlayPassTest.kt new file mode 100644 index 000000000000..9b0e2bd70f85 --- /dev/null +++ b/platform/lang-impl/testSources/com/intellij/codeInsight/hints/InlayPassTest.kt @@ -0,0 +1,43 @@ +// 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.SpacePresentation +import com.intellij.openapi.editor.EditorCustomElementRenderer +import com.intellij.openapi.editor.Inlay +import com.intellij.testFramework.fixtures.LightPlatformCodeInsightFixtureTestCase + +class InlayPassTest : LightPlatformCodeInsightFixtureTestCase() { + private val noSettings = SettingsKey("no") + + fun testBlockAndInlineElementMayBeAtSameOffset() { + 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, emptyList(), emptyList(), true) + val file = myFixture.file + 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.applyToEditor(editor, inlineElements, blockElements, false) + assertEquals(0, inlineElements.size) + assertEquals(0, blockElements.size) + } + + private val blockElements: MutableList> + get() = myFixture.editor.inlayModel.getBlockElementsInRange(0, myFixture.file.textRange.endOffset) + + private val inlineElements: MutableList> + get() = myFixture.editor.inlayModel.getInlineElementsInRange(0, myFixture.file.textRange.endOffset) +} \ No newline at end of file