KT-32368 Rework Inline hints settings // special case for lambda hints

Lambda hints are placed at the end of a line in such a way that user
can't move the caret behind it. Unfortunately, hint provision
infrastructure doesn't take this detail into account.
As a workaround, KotlinLambdasHintsProvider now returns an empty result
(sink) to the outer infrastructure and accesses editor's inlay model
itself.
This commit is contained in:
Andrei Klunnyi
2020-07-07 19:18:14 +02:00
parent d1722e3975
commit 49d4f55c87
4 changed files with 52 additions and 124 deletions
@@ -882,10 +882,6 @@ fun main(args: Array<String>) {
model("codeInsight/codeVision")
}
testClass<AbstractKotlinLambdasHintsProvider> {
model("codeInsight/hints/lambda")
}
testClass<AbstractKotlinSuspendingCallHintsProviderTest> {
model("codeInsight/hints/suspending")
}
@@ -23,6 +23,20 @@ abstract class KotlinAbstractHintsProvider<T : Any> : InlayHintsProvider<T> {
abstract fun isElementSupported(resolved: HintType?, settings: T): Boolean
/**
* By default [PresentationAndSettings] go directly to the [sink] and later are handled by the outer infrastructure.
* The thing is that for lambdas this approach doesn't work: user shouldn't be able to place the caret behind a hint.
* Therefore [KotlinLambdasHintsProvider] provides its own "crutch" implementation.
*/
protected open fun handlePresentations(
presentations: List<PresentationAndSettings>,
editor: Editor,
sink: InlayHintsSink
) {
presentations.forEach { p ->
sink.addInlineElement(p.offset, p.relatesToPrecedingText, p.presentation)
}
}
override fun getCollectorFor(file: PsiFile, editor: Editor, settings: T, sink: InlayHintsSink): InlayHintsCollector? {
return object : FactoryInlayHintsCollector(editor) {
@@ -30,13 +44,13 @@ abstract class KotlinAbstractHintsProvider<T : Any> : InlayHintsProvider<T> {
val resolved = HintType.resolve(element) ?: return true
if (!isElementSupported(resolved, settings)) return true
resolved.provideHints(element)
.mapNotNull { info -> convert(info, editor.project) }
.forEach { triple -> sink.addInlineElement(triple.first, triple.second, triple.third) }
val presentations = resolved.provideHints(element).mapNotNull { info -> convert(info, editor.project) }
if (presentations.isNotEmpty())
handlePresentations(presentations, editor, sink)
return true
}
fun convert(inlayInfo: InlayInfo, project: Project?): Triple<Int, Boolean, InlayPresentation>? {
fun convert(inlayInfo: InlayInfo, project: Project?): PresentationAndSettings? {
val inlayText = getInlayPresentation(inlayInfo.text)
val presentation = factory.roundWithBackground(factory.smallText(inlayText))
@@ -51,7 +65,7 @@ abstract class KotlinAbstractHintsProvider<T : Any> : InlayHintsProvider<T> {
}, left = 1
)
return Triple(inlayInfo.offset, inlayInfo.relatesToPrecedingText, finalPresentation)
return PresentationAndSettings(finalPresentation, inlayInfo.offset, inlayInfo.relatesToPrecedingText)
}
fun getInlayPresentation(inlayText: String): String =
@@ -62,4 +76,6 @@ abstract class KotlinAbstractHintsProvider<T : Any> : InlayHintsProvider<T> {
}
}
}
data class PresentationAndSettings(val presentation: InlayPresentation, val offset: Int, val relatesToPrecedingText: Boolean)
}
@@ -7,10 +7,31 @@ package org.jetbrains.kotlin.idea.codeInsight.hints
import com.intellij.codeInsight.hints.ChangeListener
import com.intellij.codeInsight.hints.ImmediateConfigurable
import com.intellij.codeInsight.hints.InlayHintsSink
import com.intellij.codeInsight.hints.presentation.PresentationRenderer
import com.intellij.openapi.application.TransactionGuard
import com.intellij.openapi.application.invokeLater
import com.intellij.openapi.editor.Editor
import com.intellij.ui.layout.panel
import com.sun.glass.ui.Application
import org.jetbrains.kotlin.idea.KotlinBundle
import javax.swing.JComponent
/**
* Please note that the executable test class is currently not generated. The thing is that [KotlinLambdasHintsProvider] utilizes a hack
* preventing it from being testable (see [handlePresentations]). Nevertheless [AbstractKotlinLambdasHintsProvider] and corresponding
* [testData][idea/testData/codeInsight/hints/lambda] exist.
* To run the tests in "imaginary" environment (might still be valuable):
* 1. Comment out [handlePresentations]
* 2. Add the following code snippet next to the similar ones at [GenerateTests.kt]:
* ```
* testClass<AbstractKotlinLambdasHintsProvider> {
* model("codeInsight/hints/lambda")
* }
* ```
* 3. Run "Generate All Tests". You're expected to get `KotlinLambdasHintsProviderGenerated` class.
*/
@Suppress("UnstableApiUsage")
class KotlinLambdasHintsProvider : KotlinAbstractHintsProvider<KotlinLambdasHintsProvider.Settings>() {
@@ -29,6 +50,16 @@ class KotlinLambdasHintsProvider : KotlinAbstractHintsProvider<KotlinLambdasHint
}
}
override fun handlePresentations(presentations: List<PresentationAndSettings>, editor: Editor, sink: InlayHintsSink) {
// sink should remain empty for the outer infrastructure - we place hints ourselves
invokeLater {
presentations.forEach { p ->
editor.inlayModel.getAfterLineEndElementsInRange(p.offset, p.offset).singleOrNull()?.dispose()
editor.inlayModel.addAfterLineEndElement(p.offset, p.relatesToPrecedingText, PresentationRenderer(p.presentation))
}
}
}
override fun createConfigurable(settings: Settings): ImmediateConfigurable {
return object : ImmediateConfigurable {
override fun createComponent(listener: ChangeListener): JComponent = panel {}
@@ -1,115 +0,0 @@
/*
* Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.idea.codeInsight.hints;
import com.intellij.testFramework.TestDataPath;
import org.jetbrains.kotlin.test.JUnit3RunnerWithInners;
import org.jetbrains.kotlin.test.KotlinTestUtils;
import org.jetbrains.kotlin.test.TestMetadata;
import org.junit.runner.RunWith;
import java.io.File;
import java.util.regex.Pattern;
/** This class is generated by {@link org.jetbrains.kotlin.generators.tests.TestsPackage}. DO NOT MODIFY MANUALLY */
@SuppressWarnings("all")
@TestMetadata("idea/testData/codeInsight/hints/lambda")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)
public class KotlinLambdasHintsProviderGenerated extends AbstractKotlinLambdasHintsProvider {
private void runTest(String testDataFilePath) throws Exception {
KotlinTestUtils.runTest(this::doTest, this, testDataFilePath);
}
public void testAllFilesPresentInLambda() throws Exception {
KotlinTestUtils.assertAllTestsPresentByMetadataWithExcluded(this.getClass(), new File("idea/testData/codeInsight/hints/lambda"), Pattern.compile("^(.+)\\.kt$"), null, true);
}
@TestMetadata("AnnotatedStatement.kt")
public void testAnnotatedStatement() throws Exception {
runTest("idea/testData/codeInsight/hints/lambda/AnnotatedStatement.kt");
}
@TestMetadata("DisabledHints.kt")
public void testDisabledHints() throws Exception {
runTest("idea/testData/codeInsight/hints/lambda/DisabledHints.kt");
}
@TestMetadata("Elvis.kt")
public void testElvis() throws Exception {
runTest("idea/testData/codeInsight/hints/lambda/Elvis.kt");
}
@TestMetadata("If.kt")
public void testIf() throws Exception {
runTest("idea/testData/codeInsight/hints/lambda/If.kt");
}
@TestMetadata("ImplicitIt.kt")
public void testImplicitIt() throws Exception {
runTest("idea/testData/codeInsight/hints/lambda/ImplicitIt.kt");
}
@TestMetadata("ImplicitSingleLine.kt")
public void testImplicitSingleLine() throws Exception {
runTest("idea/testData/codeInsight/hints/lambda/ImplicitSingleLine.kt");
}
@TestMetadata("ImplicitThis.kt")
public void testImplicitThis() throws Exception {
runTest("idea/testData/codeInsight/hints/lambda/ImplicitThis.kt");
}
@TestMetadata("Label.kt")
public void testLabel() throws Exception {
runTest("idea/testData/codeInsight/hints/lambda/Label.kt");
}
@TestMetadata("LabeledStatement.kt")
public void testLabeledStatement() throws Exception {
runTest("idea/testData/codeInsight/hints/lambda/LabeledStatement.kt");
}
@TestMetadata("Nested.kt")
public void testNested() throws Exception {
runTest("idea/testData/codeInsight/hints/lambda/Nested.kt");
}
@TestMetadata("NoHintForSingleExpression.kt")
public void testNoHintForSingleExpression() throws Exception {
runTest("idea/testData/codeInsight/hints/lambda/NoHintForSingleExpression.kt");
}
@TestMetadata("OneLineIf.kt")
public void testOneLineIf() throws Exception {
runTest("idea/testData/codeInsight/hints/lambda/OneLineIf.kt");
}
@TestMetadata("PostfixPrefixExpr.kt")
public void testPostfixPrefixExpr() throws Exception {
runTest("idea/testData/codeInsight/hints/lambda/PostfixPrefixExpr.kt");
}
@TestMetadata("Qualified.kt")
public void testQualified() throws Exception {
runTest("idea/testData/codeInsight/hints/lambda/Qualified.kt");
}
@TestMetadata("ReturnFunType.kt")
public void testReturnFunType() throws Exception {
runTest("idea/testData/codeInsight/hints/lambda/ReturnFunType.kt");
}
@TestMetadata("SimpleCase.kt")
public void testSimpleCase() throws Exception {
runTest("idea/testData/codeInsight/hints/lambda/SimpleCase.kt");
}
@TestMetadata("When.kt")
public void testWhen() throws Exception {
runTest("idea/testData/codeInsight/hints/lambda/When.kt");
}
}