-
Notifications
You must be signed in to change notification settings - Fork 232
Support for inlay hints #498
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 11 commits
Commits
Show all changes
21 commits
Select commit
Hold shift + click to select a range
02df3c6
add inlay hint support
elamc-2 da7e786
include lambda hints
elamc-2 ede0179
inferred lambda parameter type
elamc-2 c05c885
render non-lambda arguments of callable
elamc-2 f4d149f
handle vararg parameter
elamc-2 e74909b
fix empty callable
elamc-2 4820553
support chained methods
elamc-2 995bc9c
fix imports
elamc-2 0e58752
fix chained hints label
elamc-2 17bc488
fix lambda declaration hint
elamc-2 7c212e4
tests
elamc-2 97d37b8
review fixes
elamc-2 88e987c
lambda destructure-type parameter hint
elamc-2 25ad6d4
refactor
elamc-2 77d109a
refactor fix
elamc-2 c58f600
support single-expression functions
elamc-2 fca0ab0
refactor
elamc-2 680b3f4
add config
elamc-2 2f03112
handle destrcture declaration unused vars
elamc-2 e6a52e0
fixes
elamc-2 0f12d0a
suppress detekt rule
elamc-2 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
151 changes: 151 additions & 0 deletions
151
server/src/main/kotlin/org/javacs/kt/inlayhints/InlayHint.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,151 @@ | ||
package org.javacs.kt.inlayhints | ||
|
||
import com.intellij.psi.PsiElement | ||
import com.intellij.psi.PsiNameIdentifierOwner | ||
import com.intellij.psi.PsiWhiteSpace | ||
import org.eclipse.lsp4j.InlayHint | ||
import org.eclipse.lsp4j.InlayHintKind | ||
import org.eclipse.lsp4j.jsonrpc.messages.Either | ||
import org.javacs.kt.CompiledFile | ||
import org.javacs.kt.completion.DECL_RENDERER | ||
import org.javacs.kt.position.range | ||
import org.javacs.kt.util.preOrderTraversal | ||
import org.jetbrains.kotlin.descriptors.CallableDescriptor | ||
import org.jetbrains.kotlin.lexer.KtTokens.DOT | ||
import org.jetbrains.kotlin.psi.KtCallExpression | ||
import org.jetbrains.kotlin.psi.KtDestructuringDeclaration | ||
import org.jetbrains.kotlin.psi.KtDestructuringDeclarationEntry | ||
import org.jetbrains.kotlin.psi.KtDotQualifiedExpression | ||
import org.jetbrains.kotlin.psi.KtLambdaArgument | ||
import org.jetbrains.kotlin.psi.KtProperty | ||
import org.jetbrains.kotlin.psi.KtParameter | ||
import org.jetbrains.kotlin.psi.psiUtil.getChildOfType | ||
import org.jetbrains.kotlin.psi.psiUtil.getChildrenOfType | ||
import org.jetbrains.kotlin.resolve.BindingContext | ||
import org.jetbrains.kotlin.resolve.calls.model.VarargValueArgument | ||
import org.jetbrains.kotlin.resolve.calls.smartcasts.getKotlinTypeForComparison | ||
import org.jetbrains.kotlin.resolve.calls.util.getResolvedCall | ||
import org.jetbrains.kotlin.types.KotlinType | ||
import org.jetbrains.kotlin.types.error.ErrorType | ||
|
||
|
||
enum class InlayKind(val base: InlayHintKind) { | ||
TypeHint(InlayHintKind.Type), | ||
ParameterHint(InlayHintKind.Parameter), | ||
ChainingHint(InlayHintKind.Type), | ||
} | ||
|
||
private fun PsiElement.determineType(ctx: BindingContext): KotlinType? = | ||
when (this) { | ||
is KtCallExpression -> { | ||
this.getKotlinTypeForComparison(ctx) | ||
} | ||
is KtParameter -> { | ||
if (this.isLambdaParameter and (this.typeReference == null)) { | ||
val descriptor = ctx[BindingContext.DECLARATION_TO_DESCRIPTOR, this] as CallableDescriptor | ||
descriptor.returnType | ||
} else null | ||
} | ||
is KtDestructuringDeclarationEntry -> { | ||
val resolvedCall = ctx[BindingContext.COMPONENT_RESOLVED_CALL, this] | ||
resolvedCall?.resultingDescriptor?.returnType | ||
} | ||
is KtProperty -> { | ||
//TODO: better handling for unresolved-type error | ||
val type = this.getKotlinTypeForComparison(ctx) | ||
if (type is ErrorType) null else type | ||
} | ||
else -> null | ||
} | ||
|
||
private fun PsiElement.hintBuilder(kind: InlayKind, file: CompiledFile, label: String? = null): InlayHint? { | ||
val namedElement = ((this as? PsiNameIdentifierOwner)?.nameIdentifier ?: this) | ||
val range = range(file.parse.text, namedElement.textRange) | ||
|
||
val hint = when(kind) { | ||
InlayKind.ParameterHint -> InlayHint(range.start, Either.forLeft("$label:")) | ||
else -> | ||
this.determineType(file.compile) ?.let { | ||
InlayHint(range.end, Either.forLeft( | ||
"${(if (kind == InlayKind.TypeHint) ": " else "")}${DECL_RENDERER.renderType(it)}" | ||
)) | ||
} ?: return null | ||
} | ||
hint.kind = kind.base | ||
hint.paddingRight = true | ||
hint.paddingLeft = true | ||
return hint | ||
} | ||
|
||
private fun callableArgsToHints( | ||
callExpression: KtCallExpression, | ||
file: CompiledFile, | ||
): List<InlayHint> { | ||
val resolvedCall = callExpression.getResolvedCall(file.compile) | ||
|
||
val hints = mutableListOf<InlayHint>() | ||
resolvedCall?.valueArguments?.forEach { (t, u) -> | ||
elamc-2 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if (u.arguments.isNotEmpty()) { | ||
val valueArg = u.arguments.first() | ||
|
||
if (!valueArg.isNamed()) { | ||
val label = (t.name).let { name -> | ||
when (u) { | ||
is VarargValueArgument -> "...$name" | ||
else -> name.asString() | ||
} | ||
} | ||
valueArg.asElement().hintBuilder(InlayKind.ParameterHint, file, label)?.let { hints.add(it) } | ||
} | ||
|
||
} | ||
} | ||
return hints | ||
} | ||
|
||
private fun lambdaValueParamsToHints(node: KtLambdaArgument, file: CompiledFile): List<InlayHint> { | ||
return node.getLambdaExpression()!!.valueParameters.mapNotNull { | ||
it.hintBuilder(InlayKind.TypeHint, file) | ||
} | ||
} | ||
|
||
private fun chainedMethodsHints(node: KtDotQualifiedExpression, file: CompiledFile): List<InlayHint> { | ||
return node.getChildrenOfType<KtCallExpression>().mapNotNull { | ||
it.hintBuilder(InlayKind.ChainingHint, file) | ||
} | ||
} | ||
|
||
fun provideHints(file: CompiledFile): List<InlayHint> { | ||
val hints = mutableListOf<InlayHint>() | ||
for (node in file.parse.preOrderTraversal().asIterable()) { | ||
when (node) { | ||
is KtLambdaArgument -> { | ||
hints.addAll(lambdaValueParamsToHints(node, file)) | ||
} | ||
is KtDotQualifiedExpression -> { | ||
///chaining is defined as an expression whose next sibling tokens are newline and dot | ||
(node.nextSibling as? PsiWhiteSpace)?.let { | ||
if (it.nextSibling.node.elementType == DOT) { | ||
hints.addAll(chainedMethodsHints(node, file)) | ||
} | ||
} | ||
} | ||
is KtCallExpression -> { | ||
//hints are not rendered for argument of type lambda expression i.e. list.map { it } | ||
if (node.getChildOfType<KtLambdaArgument>() == null) { | ||
hints.addAll(callableArgsToHints(node, file)) | ||
} | ||
} | ||
is KtDestructuringDeclaration -> { | ||
hints.addAll(node.entries.mapNotNull { it.hintBuilder(InlayKind.TypeHint, file) }) | ||
} | ||
is KtProperty -> { | ||
//check decleration does not include type i.e. var t1: String | ||
if (node.typeReference == null) { | ||
node.hintBuilder(InlayKind.TypeHint, file)?.let { hints.add(it) } | ||
} | ||
} | ||
} | ||
} | ||
return hints | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,154 @@ | ||
package org.javacs.kt | ||
|
||
import org.eclipse.lsp4j.InlayHint | ||
import org.eclipse.lsp4j.Position | ||
import org.hamcrest.Matchers.isIn | ||
import org.hamcrest.Matchers.hasSize | ||
import org.hamcrest.Matchers.everyItem | ||
import org.hamcrest.Matchers.containsString | ||
import org.hamcrest.Matchers.equalTo | ||
import org.junit.Assert.assertThat | ||
import org.junit.Assert.assertTrue | ||
import org.junit.Assert.assertEquals | ||
import org.junit.Test | ||
|
||
|
||
private fun predicate(pos: Position, label: String) = { | ||
hint: InlayHint -> hint.position == pos && hint.label.left.contains(label) | ||
} | ||
|
||
private fun nPredicateFilter( | ||
hints: List<InlayHint>, | ||
predicates: List<(InlayHint) -> Boolean> | ||
): List<InlayHint> = | ||
hints.filter { | ||
predicates.any { p -> p(it) } | ||
} | ||
|
||
|
||
class InlayHintDeclarationTest : SingleFileTestFixture("inlayhints", "Declarations.kt") { | ||
|
||
private val hints = languageServer.textDocumentService.inlayHint(inlayHintParams(file, range(0, 0, 0, 0))).get() | ||
|
||
@Test | ||
fun `lambda declaration hints`() { | ||
val result = hints.filter { | ||
it.position == Position(2, 10) | ||
} | ||
assertThat(result, hasSize(1)) | ||
|
||
val label = result.single().label.left.replaceBefore("(", "") | ||
val regex = Regex("\\(([^)]+)\\) -> .*") | ||
assertTrue(label.matches(regex)) | ||
} | ||
|
||
@Test | ||
fun `destrucuted declaration hints`() { | ||
val predicates = listOf( | ||
predicate(Position(17, 10), "Float"), | ||
predicate(Position(17, 13), "Double"), | ||
) | ||
val result = nPredicateFilter(hints, predicates) | ||
assertThat(result, hasSize(2)) | ||
assertThat(result, everyItem(isIn(hints))) | ||
} | ||
|
||
@Test | ||
fun `should not render hint with explicit type`() { | ||
val result = hints.filter { | ||
it.label.left.contains("Type") | ||
} | ||
assertTrue(result.isEmpty()) | ||
} | ||
|
||
@Test | ||
fun `generic type hints`() { | ||
val expected = listOf(Position(5, 13), Position(20, 7)) | ||
|
||
val result = hints.filter { | ||
it.label.left.matches(Regex(": Box<([^)]+)>")) | ||
}.map { it.position } | ||
|
||
assertEquals(result.size, expected.size) | ||
assertEquals(result.sortedBy { it.line }, expected.sortedBy { it.line }) | ||
} | ||
|
||
} | ||
|
||
class InlayHintCallableParameterTest : SingleFileTestFixture("inlayhints", "Parameters.kt") { | ||
|
||
private val hints = languageServer.textDocumentService.inlayHint(inlayHintParams(file, range(0, 0, 0, 0))).get() | ||
|
||
@Test | ||
fun `class parameter hints`() { | ||
val predicates = listOf( | ||
predicate(Position(13, 4), "x"), | ||
predicate(Position(14, 4), "y"), | ||
predicate(Position(15, 4), "z"), | ||
) | ||
val result = nPredicateFilter(hints, predicates) | ||
assertThat(result, hasSize(3)) | ||
assertThat(result, everyItem(isIn(hints))) | ||
} | ||
|
||
@Test | ||
fun `has one vararg parameter hint`() { | ||
val varargHintCount = hints.filter { | ||
it.label.left.contains("ints") | ||
}.size | ||
assertThat(varargHintCount, equalTo(1)) | ||
} | ||
|
||
@Test | ||
fun `mixed parameter types`(){ | ||
val predicates = listOf( | ||
predicate(Position(17, 14), "d"), | ||
predicate(Position(17, 19), "p1"), | ||
predicate(Position(17, 25), "ints"), | ||
) | ||
val result = nPredicateFilter(hints, predicates) | ||
assertThat(result, hasSize(3)) | ||
assertThat(result, everyItem(isIn(hints))) | ||
} | ||
|
||
@Test | ||
fun `inferred lambda parameter type`() { | ||
val hint = hints.filter { | ||
it.label.left.contains("Int") | ||
} | ||
assertThat(hint, hasSize(1)) | ||
assertThat(hint.single().label.left, containsString("Int")) | ||
} | ||
|
||
} | ||
|
||
class InlayHintChainedTest : SingleFileTestFixture("inlayhints", "ChainedMethods.kt") { | ||
|
||
private val hints = languageServer.textDocumentService.inlayHint(inlayHintParams(file, range(0, 0, 0, 0))).get() | ||
|
||
@Test | ||
fun `chained hints`() { | ||
val predicates = listOf( | ||
predicate(Position(17, 34), "List<String>"), | ||
predicate(Position(18, 26), "List<Int>"), | ||
themkat marked this conversation as resolved.
Show resolved
Hide resolved
|
||
predicate(Position(19, 19), "Array<Int>"), | ||
) | ||
val result = nPredicateFilter(hints, predicates) | ||
|
||
assertThat(result, hasSize(3)) | ||
assertThat(result, everyItem(isIn(hints))) | ||
} | ||
|
||
@Test | ||
fun `generic chained hints`() { | ||
val predicates = listOf( | ||
predicate(Position(22, 16), "A<Int>"), | ||
predicate(Position(23, 8), "B<Int>"), | ||
) | ||
val result = nPredicateFilter(hints, predicates) | ||
|
||
assertThat(result, hasSize(2)) | ||
assertThat(result, everyItem(isIn(hints))) | ||
} | ||
|
||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
package inlayhints | ||
|
||
|
||
class A<T>(private val self: List<T>) { | ||
fun a(): B<T> { | ||
return B(self) | ||
} | ||
} | ||
|
||
class B<T>(private val self: List<T>) { | ||
fun b(): List<T> { | ||
return self | ||
} | ||
} | ||
|
||
val foo = listOf(1, 2, 3, 4) | ||
|
||
val bar = listOf("hello", "world") | ||
.map { it.length * 2 } | ||
.toTypedArray() | ||
.contains(2) | ||
|
||
val baz = A(foo) | ||
.a() | ||
.b() |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.