Skip to content

Commit bd5d987

Browse files
authored
Support for inlay hints (#498)
* add inlay hint support * include lambda hints * inferred lambda parameter type * render non-lambda arguments of callable * handle vararg parameter * fix empty callable * support chained methods * fix imports * fix chained hints label * fix lambda declaration hint * tests * review fixes * lambda destructure-type parameter hint * refactor * refactor fix * support single-expression functions * refactor * add config * handle destrcture declaration unused vars * fixes * suppress detekt rule
1 parent 323d14f commit bd5d987

File tree

10 files changed

+504
-7
lines changed

10 files changed

+504
-7
lines changed

server/src/main/kotlin/org/javacs/kt/Configuration.kt

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,12 @@ public data class ExternalSourcesConfiguration(
4646
var autoConvertToKotlin: Boolean = false
4747
)
4848

49+
data class InlayHintsConfiguration(
50+
var typeHints: Boolean = false,
51+
var parameterHints: Boolean = false,
52+
var chainedHints: Boolean = false
53+
)
54+
4955

5056
fun getStoragePath(params: InitializeParams): Path? {
5157
params.initializationOptions?.let { initializationOptions ->
@@ -81,5 +87,6 @@ public data class Configuration(
8187
val completion: CompletionConfiguration = CompletionConfiguration(),
8288
val linting: LintingConfiguration = LintingConfiguration(),
8389
var indexing: IndexingConfiguration = IndexingConfiguration(),
84-
val externalSources: ExternalSourcesConfiguration = ExternalSourcesConfiguration()
90+
val externalSources: ExternalSourcesConfiguration = ExternalSourcesConfiguration(),
91+
val hints: InlayHintsConfiguration = InlayHintsConfiguration()
8592
)

server/src/main/kotlin/org/javacs/kt/KotlinLanguageServer.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ class KotlinLanguageServer : LanguageServer, LanguageClientAware, Closeable {
7777
serverCapabilities.workspace.workspaceFolders = WorkspaceFoldersOptions()
7878
serverCapabilities.workspace.workspaceFolders.supported = true
7979
serverCapabilities.workspace.workspaceFolders.changeNotifications = Either.forRight(true)
80+
serverCapabilities.inlayHintProvider = Either.forLeft(true)
8081
serverCapabilities.hoverProvider = Either.forLeft(true)
8182
serverCapabilities.renameProvider = Either.forLeft(true)
8283
serverCapabilities.completionProvider = CompletionOptions(false, listOf("."))

server/src/main/kotlin/org/javacs/kt/KotlinTextDocumentService.kt

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import org.eclipse.lsp4j.jsonrpc.messages.Either
55
import org.eclipse.lsp4j.services.LanguageClient
66
import org.eclipse.lsp4j.services.TextDocumentService
77
import org.javacs.kt.codeaction.codeActions
8-
import org.javacs.kt.completion.*
8+
import org.javacs.kt.completion.completions
99
import org.javacs.kt.definition.goToDefinition
1010
import org.javacs.kt.diagnostic.convertDiagnostic
1111
import org.javacs.kt.formatting.formatKotlinCode
@@ -16,17 +16,18 @@ import org.javacs.kt.position.position
1616
import org.javacs.kt.references.findReferences
1717
import org.javacs.kt.semantictokens.encodedSemanticTokens
1818
import org.javacs.kt.signaturehelp.fetchSignatureHelpAt
19+
import org.javacs.kt.rename.renameSymbol
20+
import org.javacs.kt.highlight.documentHighlightsAt
21+
import org.javacs.kt.inlayhints.provideHints
1922
import org.javacs.kt.symbols.documentSymbols
20-
import org.javacs.kt.util.noResult
2123
import org.javacs.kt.util.AsyncExecutor
2224
import org.javacs.kt.util.Debouncer
23-
import org.javacs.kt.util.filePath
2425
import org.javacs.kt.util.TemporaryDirectory
25-
import org.javacs.kt.util.parseURI
2626
import org.javacs.kt.util.describeURI
2727
import org.javacs.kt.util.describeURIs
28-
import org.javacs.kt.rename.renameSymbol
29-
import org.javacs.kt.highlight.documentHighlightsAt
28+
import org.javacs.kt.util.filePath
29+
import org.javacs.kt.util.noResult
30+
import org.javacs.kt.util.parseURI
3031
import org.jetbrains.kotlin.resolve.diagnostics.Diagnostics
3132
import java.net.URI
3233
import java.io.Closeable
@@ -95,6 +96,11 @@ class KotlinTextDocumentService(
9596
codeActions(file, sp.index, params.range, params.context)
9697
}
9798

99+
override fun inlayHint(params: InlayHintParams): CompletableFuture<List<InlayHint>> = async.compute {
100+
val (file, _) = recover(params.textDocument.uri, params.range.start, Recompile.ALWAYS)
101+
provideHints(file, config.hints)
102+
}
103+
98104
override fun hover(position: HoverParams): CompletableFuture<Hover?> = async.compute {
99105
reportTime {
100106
LOG.info("Hovering at {}", describePosition(position))

server/src/main/kotlin/org/javacs/kt/KotlinWorkspaceService.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,14 @@ class KotlinWorkspaceService(
102102
}
103103
}
104104

105+
// Update options for inlay hints
106+
get("inlayHints")?.asJsonObject?.apply {
107+
val hints = config.hints
108+
get("typeHints")?.asBoolean?.let { hints.typeHints = it }
109+
get("parameterHints")?.asBoolean?.let { hints.parameterHints = it }
110+
get("chainedHints")?.asBoolean?.let { hints.chainedHints = it }
111+
}
112+
105113
// Update linter options
106114
get("linting")?.asJsonObject?.apply {
107115
val linting = config.linting
Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
package org.javacs.kt.inlayhints
2+
3+
import com.intellij.psi.PsiElement
4+
import com.intellij.psi.PsiNameIdentifierOwner
5+
import com.intellij.psi.PsiWhiteSpace
6+
import org.eclipse.lsp4j.InlayHint
7+
import org.eclipse.lsp4j.InlayHintKind
8+
import org.eclipse.lsp4j.jsonrpc.messages.Either
9+
import org.javacs.kt.CompiledFile
10+
import org.javacs.kt.InlayHintsConfiguration
11+
import org.javacs.kt.completion.DECL_RENDERER
12+
import org.javacs.kt.position.range
13+
import org.javacs.kt.util.preOrderTraversal
14+
import org.jetbrains.kotlin.descriptors.CallableDescriptor
15+
import org.jetbrains.kotlin.lexer.KtTokens.DOT
16+
import org.jetbrains.kotlin.name.Name
17+
import org.jetbrains.kotlin.psi.KtCallExpression
18+
import org.jetbrains.kotlin.psi.KtDestructuringDeclaration
19+
import org.jetbrains.kotlin.psi.KtDestructuringDeclarationEntry
20+
import org.jetbrains.kotlin.psi.KtDotQualifiedExpression
21+
import org.jetbrains.kotlin.psi.KtFunction
22+
import org.jetbrains.kotlin.psi.KtLambdaArgument
23+
import org.jetbrains.kotlin.psi.KtNamedFunction
24+
import org.jetbrains.kotlin.psi.KtProperty
25+
import org.jetbrains.kotlin.psi.KtParameter
26+
import org.jetbrains.kotlin.psi.psiUtil.getChildOfType
27+
import org.jetbrains.kotlin.psi.psiUtil.getChildrenOfType
28+
import org.jetbrains.kotlin.resolve.BindingContext
29+
import org.jetbrains.kotlin.resolve.calls.model.ResolvedValueArgument
30+
import org.jetbrains.kotlin.resolve.calls.model.VarargValueArgument
31+
import org.jetbrains.kotlin.resolve.calls.smartcasts.getKotlinTypeForComparison
32+
import org.jetbrains.kotlin.resolve.calls.util.getResolvedCall
33+
import org.jetbrains.kotlin.resolve.calls.util.isSingleUnderscore
34+
import org.jetbrains.kotlin.types.KotlinType
35+
import org.jetbrains.kotlin.types.error.ErrorType
36+
37+
38+
private fun PsiElement.determineType(ctx: BindingContext): KotlinType? =
39+
when (this) {
40+
is KtNamedFunction -> {
41+
val descriptor = ctx[BindingContext.FUNCTION, this]
42+
descriptor?.returnType
43+
}
44+
is KtCallExpression -> {
45+
this.getKotlinTypeForComparison(ctx)
46+
}
47+
is KtParameter -> {
48+
if (this.isLambdaParameter and (this.typeReference == null)) {
49+
val descriptor = ctx[BindingContext.DECLARATION_TO_DESCRIPTOR, this] as CallableDescriptor
50+
descriptor.returnType
51+
} else null
52+
}
53+
is KtDestructuringDeclarationEntry -> {
54+
//skip unused variable denoted by underscore
55+
//https://kotlinlang.org/docs/destructuring-declarations.html#underscore-for-unused-variables
56+
if (this.isSingleUnderscore) {
57+
null
58+
} else {
59+
val resolvedCall = ctx[BindingContext.COMPONENT_RESOLVED_CALL, this]
60+
resolvedCall?.resultingDescriptor?.returnType
61+
}
62+
}
63+
is KtProperty -> {
64+
val type = this.getKotlinTypeForComparison(ctx)
65+
if (type is ErrorType) null else type
66+
}
67+
else -> null
68+
}
69+
70+
@Suppress("ReturnCount")
71+
private fun PsiElement.hintBuilder(kind: InlayKind, file: CompiledFile, label: String? = null): InlayHint? {
72+
val element = when(this) {
73+
is KtFunction -> this.valueParameterList!!.originalElement
74+
is PsiNameIdentifierOwner -> this.nameIdentifier
75+
else -> this
76+
} ?: return null
77+
78+
val range = range(file.parse.text, element.textRange)
79+
80+
val hint = when(kind) {
81+
InlayKind.ParameterHint -> InlayHint(range.start, Either.forLeft("$label:"))
82+
else ->
83+
this.determineType(file.compile) ?.let {
84+
InlayHint(range.end, Either.forLeft(DECL_RENDERER.renderType(it)))
85+
} ?: return null
86+
}
87+
hint.kind = kind.base
88+
hint.paddingRight = true
89+
hint.paddingLeft = true
90+
return hint
91+
}
92+
93+
@Suppress("ReturnCount")
94+
private fun callableArgNameHints(
95+
acc: MutableList<InlayHint>,
96+
callExpression: KtCallExpression,
97+
file: CompiledFile,
98+
config: InlayHintsConfiguration
99+
) {
100+
if (!config.parameterHints) return
101+
102+
//hints are not rendered for argument of type lambda expression i.e. list.map { it }
103+
if (callExpression.getChildOfType<KtLambdaArgument>() != null) {
104+
return
105+
}
106+
107+
val resolvedCall = callExpression.getResolvedCall(file.compile)
108+
val entries = resolvedCall?.valueArguments?.entries ?: return
109+
110+
val hints = entries.mapNotNull { (t, u) ->
111+
val valueArg = u.arguments.firstOrNull()
112+
if (valueArg != null && !valueArg.isNamed()) {
113+
val label = getArgLabel(t.name, u)
114+
valueArg.asElement().hintBuilder(InlayKind.ParameterHint, file, label)
115+
} else null
116+
}
117+
acc.addAll(hints)
118+
}
119+
120+
private fun getArgLabel(name: Name, arg: ResolvedValueArgument) =
121+
(name).let {
122+
when (arg) {
123+
is VarargValueArgument -> "...$it"
124+
else -> it.asString()
125+
}
126+
}
127+
128+
private fun lambdaValueParamHints(
129+
acc: MutableList<InlayHint>,
130+
node: KtLambdaArgument,
131+
file: CompiledFile,
132+
config: InlayHintsConfiguration
133+
) {
134+
if (!config.typeHints) return
135+
136+
val params = node.getLambdaExpression()!!.valueParameters
137+
138+
//hint should not be rendered when parameter is of type DestructuringDeclaration
139+
//example: Map.forEach { (k,v) -> _ }
140+
//lambda parameter (k,v) becomes (k :hint, v :hint) :hint <- outer hint isnt needed
141+
params.singleOrNull()?.let {
142+
if (it.destructuringDeclaration != null) return
143+
}
144+
145+
val hints = params.mapNotNull {
146+
it.hintBuilder(InlayKind.TypeHint, file)
147+
}
148+
acc.addAll(hints)
149+
}
150+
151+
private fun chainedExpressionHints(
152+
acc: MutableList<InlayHint>,
153+
node: KtDotQualifiedExpression,
154+
file: CompiledFile,
155+
config: InlayHintsConfiguration
156+
) {
157+
if (!config.chainedHints) return
158+
159+
///chaining is defined as an expression whose next sibling tokens are newline and dot
160+
val next = (node.nextSibling as? PsiWhiteSpace)
161+
val nextSiblingElement = next?.nextSibling?.node?.elementType
162+
163+
if (nextSiblingElement != null && nextSiblingElement == DOT) {
164+
val hints = node.getChildrenOfType<KtCallExpression>().mapNotNull {
165+
it.hintBuilder(InlayKind.ChainingHint, file)
166+
}
167+
acc.addAll(hints)
168+
}
169+
}
170+
171+
private fun destructuringVarHints(
172+
acc: MutableList<InlayHint>,
173+
node: KtDestructuringDeclaration,
174+
file: CompiledFile,
175+
config: InlayHintsConfiguration
176+
) {
177+
if (!config.typeHints) return
178+
179+
val hints = node.entries.mapNotNull {
180+
it.hintBuilder(InlayKind.TypeHint, file)
181+
}
182+
acc.addAll(hints)
183+
}
184+
185+
@Suppress("ReturnCount")
186+
private fun declarationHint(
187+
acc: MutableList<InlayHint>,
188+
node: KtProperty,
189+
file: CompiledFile,
190+
config: InlayHintsConfiguration
191+
) {
192+
if (!config.typeHints) return
193+
194+
//check decleration does not include type i.e. var t1: String
195+
if (node.typeReference != null) return
196+
197+
val hint = node.hintBuilder(InlayKind.TypeHint, file) ?: return
198+
acc.add(hint)
199+
}
200+
201+
private fun functionHint(
202+
acc: MutableList<InlayHint>,
203+
node: KtNamedFunction,
204+
file: CompiledFile,
205+
config: InlayHintsConfiguration
206+
) {
207+
if (!config.typeHints) return
208+
209+
//only render hints for functions without block body
210+
//functions WITH block body will always specify return types explicitly
211+
if (!node.hasDeclaredReturnType() && !node.hasBlockBody()) {
212+
val hint = node.hintBuilder(InlayKind.TypeHint, file) ?: return
213+
acc.add(hint)
214+
}
215+
}
216+
217+
fun provideHints(file: CompiledFile, config: InlayHintsConfiguration): List<InlayHint> {
218+
val res = mutableListOf<InlayHint>()
219+
for (node in file.parse.preOrderTraversal().asIterable()) {
220+
when (node) {
221+
is KtNamedFunction -> functionHint(res, node, file, config)
222+
is KtLambdaArgument -> lambdaValueParamHints(res, node, file, config)
223+
is KtDotQualifiedExpression -> chainedExpressionHints(res, node, file, config)
224+
is KtCallExpression -> callableArgNameHints(res, node, file, config)
225+
is KtDestructuringDeclaration -> destructuringVarHints(res, node, file, config)
226+
is KtProperty -> declarationHint(res, node, file, config)
227+
}
228+
}
229+
return res
230+
}
231+
232+
enum class InlayKind(val base: InlayHintKind) {
233+
TypeHint(InlayHintKind.Type),
234+
ParameterHint(InlayHintKind.Parameter),
235+
ChainingHint(InlayHintKind.Type),
236+
}

0 commit comments

Comments
 (0)