// Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. package com.intellij.codeInsight.hints import com.intellij.codeInsight.hints.presentation.RecursivelyUpdatingRootPresentation import com.intellij.codeInsight.hints.presentation.RootInlayPresentation import com.intellij.codeInsight.hints.presentation.SpacePresentation import com.intellij.lang.Language import com.intellij.openapi.actionSystem.ex.ActionUtil import com.intellij.openapi.editor.Editor import com.intellij.openapi.editor.Inlay import com.intellij.openapi.progress.DumbProgressIndicator import com.intellij.psi.PsiElement import com.intellij.testFramework.fixtures.BasePlatformTestCase import java.util.concurrent.atomic.AtomicBoolean class InlayPassTest : BasePlatformTestCase() { private val noSettings = SettingsKey("no") private lateinit var sharedSink: InlayHintsSinkImpl override fun setUp() { super.setUp() myFixture.configureByText("file.java", "class A{ }") sharedSink = InlayHintsSinkImpl(myFixture.editor) } fun testTurnedOffHintsDisappear() { 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 testNonProviderMangedInlayStayUntouched() { val presentation = HorizontalConstrainedPresentation(TestRootPresentation(), null) @Suppress("UNCHECKED_CAST") inlayModel.addInlineElement(3, InlineInlayRenderer(listOf(presentation as HorizontalConstrainedPresentation))) createPass(emptyList()).collectAndApply() assertEquals(1, inlineElements.size) val renderer = inlineElements.first().renderer as InlineInlayRenderer assertEquals(presentation, renderer.getConstrainedPresentations().first()) } 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)) } fun testInlaysRespectHorizontalConstraints() { createPass(listOf(createOneOffCollector { it.addInlineElement(1, true, TestRootPresentation(1), true) it.addInlineElement(2, true, TestRootPresentation(2), false) it.addInlineElement(3, false, TestRootPresentation(3), false) })).collectAndApply() var inlays = inlineElements assertEquals(2, inlays.size) assertTrue(inlays[0].isRelatedToPrecedingText) assertFalse(inlays[1].isRelatedToPrecedingText) inlays = afterLineEndElements assertEquals(1, inlays.size) } fun testInlaysAtSameOffsetWithMatchingRelatesToPrecedingText() { createPass(listOf(createOneOffCollector { it.addInlineElement(1, true, TestRootPresentation(1), false) it.addInlineElement(1, true, TestRootPresentation(2), false) })).collectAndApply() val inlays = inlineElements assertEquals(1, inlays.size) assertTrue(inlays[0].isRelatedToPrecedingText) } fun testInlaysAtSameOffsetWithDifferentRelatesToPrecedingTextDefaultToFalse() { createPass(listOf(createOneOffCollector { it.addInlineElement(1, true, TestRootPresentation(1), false) it.addInlineElement(1, false, TestRootPresentation(2), false) })).collectAndApply() val inlays = inlineElements assertEquals(1, inlays.size) assertFalse(inlays[0].isRelatedToPrecedingText) } 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)) } fun testNoPresentationRecreatedWhenNothingChanges() { val initilalRoot = RecursivelyUpdatingRootPresentation(SpacePresentation(10, 10)) applyPassWithCollectorOfSingleElement(initilalRoot) applyPassWithCollectorOfSingleElement(RecursivelyUpdatingRootPresentation(SpacePresentation(10, 10))) val root = expectSingleHorizontalPresentation() assertSame(initilalRoot, root) } 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 { val firstTime = AtomicBoolean(true) val collectorLambda: (PsiElement, Editor, InlayHintsSink) -> Boolean = { _, _, sink -> if (firstTime.compareAndSet(true, false)) { collector(sink) } 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, sharedSink) } private fun createPass(collectors: List>): InlayHintsPass { val visibleRange = myFixture.editor.calculateVisibleRange() return ActionUtil.underModalProgress(project, "") { InlayHintsPass(myFixture.file, collectors, myFixture.editor, visibleRange, sharedSink) } } private fun InlayHintsPass.collectAndApply() { val dumbProgressIndicator = DumbProgressIndicator() doCollectInformation(dumbProgressIndicator) applyInformationToEditor() sharedSink.reset() } 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 afterLineEndElements: List> get() = inlayModel.getAfterLineEndElementsInRange(0, myFixture.file.textRange.endOffset) private val allHintsCount: Int get() = inlineElements.size + blockElements.size }