-
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 all 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
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
236 changes: 236 additions & 0 deletions
236
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,236 @@ | ||
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.InlayHintsConfiguration | ||
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.name.Name | ||
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.KtFunction | ||
import org.jetbrains.kotlin.psi.KtLambdaArgument | ||
import org.jetbrains.kotlin.psi.KtNamedFunction | ||
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.ResolvedValueArgument | ||
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.resolve.calls.util.isSingleUnderscore | ||
import org.jetbrains.kotlin.types.KotlinType | ||
import org.jetbrains.kotlin.types.error.ErrorType | ||
|
||
|
||
private fun PsiElement.determineType(ctx: BindingContext): KotlinType? = | ||
when (this) { | ||
is KtNamedFunction -> { | ||
val descriptor = ctx[BindingContext.FUNCTION, this] | ||
descriptor?.returnType | ||
} | ||
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 -> { | ||
//skip unused variable denoted by underscore | ||
//https://kotlinlang.org/docs/destructuring-declarations.html#underscore-for-unused-variables | ||
if (this.isSingleUnderscore) { | ||
null | ||
} else { | ||
val resolvedCall = ctx[BindingContext.COMPONENT_RESOLVED_CALL, this] | ||
resolvedCall?.resultingDescriptor?.returnType | ||
} | ||
} | ||
is KtProperty -> { | ||
val type = this.getKotlinTypeForComparison(ctx) | ||
if (type is ErrorType) null else type | ||
} | ||
else -> null | ||
} | ||
|
||
@Suppress("ReturnCount") | ||
private fun PsiElement.hintBuilder(kind: InlayKind, file: CompiledFile, label: String? = null): InlayHint? { | ||
val element = when(this) { | ||
is KtFunction -> this.valueParameterList!!.originalElement | ||
is PsiNameIdentifierOwner -> this.nameIdentifier | ||
else -> this | ||
} ?: return null | ||
|
||
val range = range(file.parse.text, element.textRange) | ||
|
||
val hint = when(kind) { | ||
InlayKind.ParameterHint -> InlayHint(range.start, Either.forLeft("$label:")) | ||
else -> | ||
this.determineType(file.compile) ?.let { | ||
InlayHint(range.end, Either.forLeft(DECL_RENDERER.renderType(it))) | ||
} ?: return null | ||
} | ||
hint.kind = kind.base | ||
hint.paddingRight = true | ||
hint.paddingLeft = true | ||
return hint | ||
} | ||
|
||
@Suppress("ReturnCount") | ||
private fun callableArgNameHints( | ||
acc: MutableList<InlayHint>, | ||
callExpression: KtCallExpression, | ||
file: CompiledFile, | ||
config: InlayHintsConfiguration | ||
) { | ||
if (!config.parameterHints) return | ||
|
||
//hints are not rendered for argument of type lambda expression i.e. list.map { it } | ||
if (callExpression.getChildOfType<KtLambdaArgument>() != null) { | ||
return | ||
} | ||
|
||
val resolvedCall = callExpression.getResolvedCall(file.compile) | ||
val entries = resolvedCall?.valueArguments?.entries ?: return | ||
|
||
val hints = entries.mapNotNull { (t, u) -> | ||
val valueArg = u.arguments.firstOrNull() | ||
if (valueArg != null && !valueArg.isNamed()) { | ||
val label = getArgLabel(t.name, u) | ||
valueArg.asElement().hintBuilder(InlayKind.ParameterHint, file, label) | ||
} else null | ||
} | ||
acc.addAll(hints) | ||
} | ||
|
||
private fun getArgLabel(name: Name, arg: ResolvedValueArgument) = | ||
(name).let { | ||
when (arg) { | ||
is VarargValueArgument -> "...$it" | ||
else -> it.asString() | ||
} | ||
} | ||
|
||
private fun lambdaValueParamHints( | ||
acc: MutableList<InlayHint>, | ||
node: KtLambdaArgument, | ||
file: CompiledFile, | ||
config: InlayHintsConfiguration | ||
) { | ||
if (!config.typeHints) return | ||
|
||
val params = node.getLambdaExpression()!!.valueParameters | ||
|
||
//hint should not be rendered when parameter is of type DestructuringDeclaration | ||
//example: Map.forEach { (k,v) -> _ } | ||
//lambda parameter (k,v) becomes (k :hint, v :hint) :hint <- outer hint isnt needed | ||
params.singleOrNull()?.let { | ||
if (it.destructuringDeclaration != null) return | ||
} | ||
|
||
val hints = params.mapNotNull { | ||
it.hintBuilder(InlayKind.TypeHint, file) | ||
} | ||
acc.addAll(hints) | ||
} | ||
|
||
private fun chainedExpressionHints( | ||
acc: MutableList<InlayHint>, | ||
node: KtDotQualifiedExpression, | ||
file: CompiledFile, | ||
config: InlayHintsConfiguration | ||
) { | ||
if (!config.chainedHints) return | ||
|
||
///chaining is defined as an expression whose next sibling tokens are newline and dot | ||
val next = (node.nextSibling as? PsiWhiteSpace) | ||
val nextSiblingElement = next?.nextSibling?.node?.elementType | ||
|
||
if (nextSiblingElement != null && nextSiblingElement == DOT) { | ||
val hints = node.getChildrenOfType<KtCallExpression>().mapNotNull { | ||
it.hintBuilder(InlayKind.ChainingHint, file) | ||
} | ||
acc.addAll(hints) | ||
} | ||
} | ||
|
||
private fun destructuringVarHints( | ||
acc: MutableList<InlayHint>, | ||
node: KtDestructuringDeclaration, | ||
file: CompiledFile, | ||
config: InlayHintsConfiguration | ||
) { | ||
if (!config.typeHints) return | ||
|
||
val hints = node.entries.mapNotNull { | ||
it.hintBuilder(InlayKind.TypeHint, file) | ||
} | ||
acc.addAll(hints) | ||
} | ||
|
||
@Suppress("ReturnCount") | ||
private fun declarationHint( | ||
acc: MutableList<InlayHint>, | ||
node: KtProperty, | ||
file: CompiledFile, | ||
config: InlayHintsConfiguration | ||
) { | ||
if (!config.typeHints) return | ||
|
||
//check decleration does not include type i.e. var t1: String | ||
if (node.typeReference != null) return | ||
|
||
val hint = node.hintBuilder(InlayKind.TypeHint, file) ?: return | ||
acc.add(hint) | ||
} | ||
|
||
private fun functionHint( | ||
acc: MutableList<InlayHint>, | ||
node: KtNamedFunction, | ||
file: CompiledFile, | ||
config: InlayHintsConfiguration | ||
) { | ||
if (!config.typeHints) return | ||
|
||
//only render hints for functions without block body | ||
//functions WITH block body will always specify return types explicitly | ||
if (!node.hasDeclaredReturnType() && !node.hasBlockBody()) { | ||
val hint = node.hintBuilder(InlayKind.TypeHint, file) ?: return | ||
acc.add(hint) | ||
} | ||
} | ||
|
||
fun provideHints(file: CompiledFile, config: InlayHintsConfiguration): List<InlayHint> { | ||
val res = mutableListOf<InlayHint>() | ||
for (node in file.parse.preOrderTraversal().asIterable()) { | ||
when (node) { | ||
is KtNamedFunction -> functionHint(res, node, file, config) | ||
is KtLambdaArgument -> lambdaValueParamHints(res, node, file, config) | ||
is KtDotQualifiedExpression -> chainedExpressionHints(res, node, file, config) | ||
is KtCallExpression -> callableArgNameHints(res, node, file, config) | ||
is KtDestructuringDeclaration -> destructuringVarHints(res, node, file, config) | ||
is KtProperty -> declarationHint(res, node, file, config) | ||
} | ||
} | ||
return res | ||
} | ||
|
||
enum class InlayKind(val base: InlayHintKind) { | ||
TypeHint(InlayHintKind.Type), | ||
ParameterHint(InlayHintKind.Parameter), | ||
ChainingHint(InlayHintKind.Type), | ||
} |
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.