Inlay hints: make it possible to have block and inline element for the same offset for single provider

GitOrigin-RevId: 36a2a318279552bc5bf18449f302347a526a87ec
This commit is contained in:
Roman.Ivanov
2019-06-14 13:31:58 +03:00
committed by intellij-monorepo-bot
parent efcffb291a
commit 9cdf4b2e36
3 changed files with 113 additions and 23 deletions
@@ -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)
@@ -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<T>(val key: SettingsKey<T>) : InlayHintsSink {
private val hints = TIntObjectHashMap<InlayHint>()
private val hints = TIntObjectHashMap<HintsAtOffset>()
override fun addInlineElement(offset: Int, relatesToPrecedingText: Boolean, presentation: InlayPresentation) {
addHint(InlineElement(offset, relatesToPrecedingText, presentation))
@@ -48,10 +51,37 @@ class InlayHintsSinkImpl<T>(val key: SettingsKey<T>) : 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<Inlay<EditorCustomElementRenderer>>,
existingVerticalInlays: List<Inlay<EditorCustomElementRenderer>>,
@@ -66,25 +96,35 @@ class InlayHintsSinkImpl<T>(val key: SettingsKey<T>) : 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<PresentationRenderer>? {
val presentation = hint.presentation
val presentationRenderer = PresentationRenderer(presentation)
val inlay = when (hint) {
is InlineElement -> inlayModel.addInlineElement(offset, hint.relatesToPrecedingText, presentationRenderer)
is BlockElement -> inlayModel.addBlockElement(
offset,
hint.relatesToPrecedingText,
hint.showAbove,
hint.priority,
presentationRenderer
)
} ?: return null
inlay.putUserData(INLAY_KEY, key)
presentation.addListener(InlayListener(inlay))
return inlay
}
class InlayListener(private val inlay: Inlay<PresentationRenderer>) : PresentationListener {
// TODO be more accurate during invalidation (requires changes in Inlay)
override fun contentChanged(area: Rectangle) = inlay.repaint()
@@ -106,13 +146,18 @@ class InlayHintsSinkImpl<T>(val key: SettingsKey<T>) : 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<T>(val key: SettingsKey<T>) : InlayHintsSink {
companion object {
private val INLAY_KEY: Key<Any?> = Key.create("INLAY_KEY")
private const val BulkChangeThreshold = 1000
@JvmField val LOG = logger<InlayHintsSinkImpl<*>>()
}
}
@@ -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<NoSettings>("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<Inlay<EditorCustomElementRenderer>>
get() = myFixture.editor.inlayModel.getBlockElementsInRange(0, myFixture.file.textRange.endOffset)
private val inlineElements: MutableList<Inlay<EditorCustomElementRenderer>>
get() = myFixture.editor.inlayModel.getInlineElementsInRange(0, myFixture.file.textRange.endOffset)
}