[lsp] implement signature help for Kotlin

^LSP-171 fixed

GitOrigin-RevId: 1b8eda93c1cc33224ff8aa501cfc7f3d26637f04
This commit is contained in:
Ilya Kirillov
2025-07-08 12:59:13 +00:00
committed by intellij-monorepo-bot
parent 4259ed05dd
commit 2ff9478864
4 changed files with 564 additions and 160 deletions
@@ -3,6 +3,7 @@ package com.jetbrains.lsp.protocol
import kotlinx.serialization.*
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.builtins.serializer
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
@Serializable
@@ -632,8 +633,14 @@ value class TextEditOrInsertReplaceEdit private constructor(val edit: JsonElemen
@Serializable
@JvmInline
value class StringOrMarkupContent private constructor(val content: JsonElement) {
constructor(content: String) : this(JsonPrimitive(content))
constructor(content: MarkupContent) : this(LSP.json.encodeToJsonElement(MarkupContent.serializer(), content))
constructor(content: String) : this(JsonPrimitive(content))
constructor(content: MarkupContent) : this(LSP.json.encodeToJsonElement(MarkupContent.serializer(), content))
fun contentAsString(): String = when (content) {
is JsonPrimitive -> content.content
is JsonObject -> LSP.json.decodeFromJsonElement(MarkupContent.serializer(), content).value
else -> error("Unexpected content type: ${content::class.simpleName}")
}
}
class CompletionItemKindSerializer : EnumAsIntSerializer<CompletionItemKind>(
@@ -244,7 +244,6 @@ data class ServerCapabilities(
typealias HoverOptions = Unknown
typealias SignatureHelpOptions = Unknown
typealias TypeDefinitionOptions = Unknown
typealias ImplementationOptions = Unknown
typealias DocumentHighlightOptions = Unknown
@@ -0,0 +1,296 @@
package com.jetbrains.lsp.protocol
import kotlinx.serialization.DeserializationStrategy
import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.SerializationException
import kotlinx.serialization.builtins.ListSerializer
import kotlinx.serialization.builtins.nullable
import kotlinx.serialization.builtins.serializer
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.descriptors.listSerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonContentPolymorphicSerializer
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonPrimitive
@Serializable
data class SignatureHelpOptions(
/**
* The characters that trigger signature help
* automatically.
*/
val triggerCharacters: List<String>,
/**
* List of characters that re-trigger signature help.
*
* These trigger characters are only active when signature help is already
* showing. All trigger characters are also counted as re-trigger
* characters.
*
* @since 3.15.0
*/
val retriggerCharacters: List<String>,
override val workDoneProgress: Boolean?,
) : WorkDoneProgressOptions
@Serializable
data class SignatureHelpParams(
/**
* The signature help context. This is only available if the client
* specifies to send this using the client capability
* `textDocument.signatureHelp.contextSupport === true`
*
* @since 3.15.0
*/
val context: SignatureHelpContext?,
override val textDocument: TextDocumentIdentifier,
override val position: Position,
override val workDoneToken: ProgressToken?,
) : TextDocumentPositionParams, WorkDoneProgressParams
/**
* How a signature help was triggered.
*
* @since 3.15.0
*/
@Serializable(with = SignatureHelpTriggerKind.Serializer::class)
enum class SignatureHelpTriggerKind(val value: Int) {
/**
* Signature help was invoked manually by the user or by a command.
*/
Invoked(1),
/**
* Signature help was triggered by a trigger character.
*/
TriggerCharacter(2),
/**
* Signature help was triggered by the cursor moving or by the document
* content changing.
*/
ContentChange(3),
;
class Serializer : EnumAsIntSerializer<SignatureHelpTriggerKind>(
serialName = SignatureHelpTriggerKind::class.simpleName!!,
serialize = SignatureHelpTriggerKind::value,
deserialize = { entries[it - 1] },
)
}
/**
* Additional information about the context in which a signature help request
* was triggered.
*
* @since 3.15.0
*/
@Serializable
data class SignatureHelpContext(
/**
* Action that caused signature help to be triggered.
*/
val triggerKind: SignatureHelpTriggerKind,
/**
* Character that caused signature help to be triggered.
*
* This is undefined when triggerKind !==
* SignatureHelpTriggerKind.TriggerCharacter
*/
val triggerCharacter: String?,
/**
* `true` if signature help was already showing when it was triggered.
*
* Retriggers occur when the signature help is already active and can be
* caused by actions such as typing a trigger character, a cursor move, or
* document content changes.
*/
val isRetrigger: Boolean,
/**
* The currently active `SignatureHelp`.
*
* The `activeSignatureHelp` has its `SignatureHelp.activeSignature` field
* updated based on the user navigating through available signatures.
*/
val activeSignatureHelp: SignatureHelp?,
)
/**
* Signature help represents the signature of something
* callable. There can be multiple signature but only one
* active and only one active parameter.
*/
@Serializable
data class SignatureHelp(
/**
* One or more signatures. If no signatures are available the signature help
* request should return `null`.
*/
val signatures: List<SignatureInformation>,
/**
* The active signature. If omitted or the value lies outside the
* range of `signatures` the value defaults to zero or is ignore if
* the `SignatureHelp` as no signatures.
*
* Whenever possible implementors should make an active decision about
* the active signature and shouldn't rely on a default value.
*
* In future version of the protocol this property might become
* mandatory to better express this.
*/
val activeSignature: Int?,
/**
* The active parameter of the active signature. If omitted or the value
* lies outside the range of `signatures[activeSignature].parameters`
* defaults to 0 if the active signature has parameters. If
* the active signature has no parameters it is ignored.
* In future version of the protocol this property might become
* mandatory to better express the active parameter if the
* active signature does have any.
*/
val activeParameter: Int?,
) {
init {
require(activeSignature == null || activeSignature >= 0) {
"activeSignature must be non-negative but was $activeSignature"
}
require(activeParameter == null || activeParameter >= 0) {
"activeParameter must be non-negative but was $activeParameter"
}
}
}
/**
* Represents the signature of something callable. A signature
* can have a label, like a function-name, a doc-comment, and
* a set of parameters.
*/
@Serializable
data class SignatureInformation(
/**
* The label of this signature. Will be shown in
* the UI.
*/
val label: String,
/**
* The human-readable doc-comment of this signature. Will be shown
* in the UI but can be omitted.
*/
val documentation: StringOrMarkupContent?,
/**
* The parameters of this signature.
*/
val parameters: List<ParameterInformation>?,
/**
* The index of the active parameter.
*
* If provided, this is used in place of `SignatureHelp.activeParameter`.
*
* @since 3.16.0
*/
val activeParameter: Int?,
) {
init {
require(activeParameter == null || activeParameter >= 0) {
"activeParameter must be non-negative but was $activeParameter"
}
}
}
/**
* Represents a parameter of a callable-signature. A parameter can
* have a label and a doc-comment.
*/
@Serializable
data class ParameterInformation(
/**
* The label of this parameter information.
*
* Either a string or an inclusive start and exclusive end offsets within
* its containing signature label. (see SignatureInformation.label). The
* offsets are based on a UTF-16 string representation as `Position` and
* `Range` does.
*
* *Note*: a label of type string should be a substring of its containing
* signature label. Its intended use case is to highlight the parameter
* label part in the `SignatureInformation.label`.
*/
val label: Label,
/**
* The human-readable doc-comment of this parameter. Will be shown
* in the UI but can be omitted.
*/
val documentation: StringOrMarkupContent?,
) {
@Serializable(with = Label.LabelSerializer::class)
sealed interface Label {
@Serializable
@JvmInline
value class StringLabel(val value: String) : Label
@Serializable(with = RangeLabel.Serializer::class)
data class RangeLabel(val start: Int, val end: Int) : Label {
init {
require(start >= 0) {
"start must be non-negative but was $start"
}
require(end >= 0) {
"end must be non-negative but was $end"
}
require(start <= end) {
"start must be less than or equal to end but was start=$start, end=$end"
}
}
object Serializer : KSerializer<RangeLabel> {
private val listSerializer = ListSerializer(Int.serializer())
override val descriptor: SerialDescriptor = listSerialDescriptor<Int>()
override fun serialize(encoder: Encoder, value: RangeLabel) {
encoder.encodeSerializableValue(listSerializer, listOf(value.start, value.end))
}
override fun deserialize(decoder: Decoder): RangeLabel {
val list = decoder.decodeSerializableValue(listSerializer)
require(list.size == 2) {
"Expected list of size 2 for RangeLabel, got ${list.size}"
}
return RangeLabel(list[0], list[1])
}
}
}
/**
* We serialize [RangeLabel] as an array, and thus we cannot use default polymorphic serializer as it writes the `kind` field
* which is not expected possible for the array case. So, we have a custom serializer.
*/
object LabelSerializer : JsonContentPolymorphicSerializer<Label>(Label::class) {
override fun selectDeserializer(element: JsonElement): DeserializationStrategy<Label> {
return when (element) {
is JsonPrimitive -> StringLabel.serializer()
is JsonArray -> RangeLabel.serializer()
else -> throw SerializationException("Unknown Label variant: $element")
}
}
}
}
}
val SignatureHelpRequest: RequestType<SignatureHelpParams, SignatureHelp?, Unit> =
RequestType("textDocument/signatureHelp", SignatureHelpParams.serializer(), SignatureHelp.serializer().nullable, Unit.serializer())
@@ -1,4 +1,4 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.kotlin.idea.parameterInfo
import com.intellij.codeInsight.CodeInsightBundle
@@ -7,6 +7,7 @@ import com.intellij.lang.parameterInfo.ParameterInfoHandlerWithTabActionSupport
import com.intellij.lang.parameterInfo.ParameterInfoUIContext
import com.intellij.lang.parameterInfo.UpdateParameterInfoContext
import com.intellij.openapi.util.registry.Registry
import com.intellij.psi.PsiElement
import com.intellij.psi.util.PsiTreeUtil
import com.intellij.ui.Gray
import com.intellij.ui.JBColor
@@ -18,7 +19,9 @@ import org.jetbrains.kotlin.analysis.api.components.KaSubtypingErrorTypePolicy
import org.jetbrains.kotlin.analysis.api.renderer.types.impl.KaTypeRendererForSource
import org.jetbrains.kotlin.analysis.api.signatures.KaVariableSignature
import org.jetbrains.kotlin.analysis.api.symbols.KaClassSymbol
import org.jetbrains.kotlin.analysis.api.symbols.KaConstructorSymbol
import org.jetbrains.kotlin.analysis.api.symbols.KaValueParameterSymbol
import org.jetbrains.kotlin.analysis.api.symbols.name
import org.jetbrains.kotlin.analysis.api.types.KaErrorType
import org.jetbrains.kotlin.config.LanguageFeature
import org.jetbrains.kotlin.descriptors.annotations.AnnotationUseSiteTarget
@@ -34,6 +37,7 @@ import org.jetbrains.kotlin.name.StandardClassIds
import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.psi.psiUtil.allChildren
import org.jetbrains.kotlin.psi.psiUtil.startOffset
import org.jetbrains.kotlin.renderer.render
import org.jetbrains.kotlin.types.Variance
import java.awt.Color
import kotlin.math.min
@@ -59,7 +63,7 @@ class KotlinHighLevelLambdaParameterInfoHandler :
override fun getArgumentListAllowedParentClasses(): Set<Class<KtLambdaArgument>> = setOf(KtLambdaArgument::class.java)
override fun getCurrentArgumentIndex(context: UpdateParameterInfoContext, argumentList: KtLambdaArgument): Int {
override fun getCurrentArgumentIndex(offset: Int, argumentList: KtLambdaArgument): Int {
val size = (argumentList.parent as? KtCallElement)?.valueArguments?.size ?: 1
return size - 1
}
@@ -99,6 +103,9 @@ abstract class KotlinHighLevelParameterInfoWithCallHandlerBase<TArgumentList : K
private val ANNOTATION_TARGET_TYPE = CallableId(StandardClassIds.AnnotationTarget, Name.identifier(AnnotationTarget.TYPE.name))
private val ANNOTATION_TARGET_VALUE_PARAMETER = CallableId(StandardClassIds.AnnotationTarget, Name.identifier(AnnotationTarget.VALUE_PARAMETER.name))
@OptIn(KaExperimentalApi::class)
private val typeRenderer = KaTypeRendererForSource.WITH_SHORT_NAMES
}
override fun getActualParameterDelimiterType(): KtSingleValueToken = KtTokens.COMMA
@@ -114,9 +121,7 @@ abstract class KotlinHighLevelParameterInfoWithCallHandlerBase<TArgumentList : K
override fun findElementForParameterInfo(context: CreateParameterInfoContext): TArgumentList? {
val file = context.file as? KtFile ?: return null
val token = file.findElementAt(context.offset) ?: return null
val argumentList = PsiTreeUtil.getParentOfType(token, argumentListClass.java, true, *STOP_SEARCH_CLASSES.toTypedArray())
?: return null
val argumentList = findElementForParameterInfo(file, context.offset) ?: return null
val callElement = argumentList.parent as? KtElement ?: return null
return analyze(callElement) {
@@ -138,6 +143,11 @@ abstract class KotlinHighLevelParameterInfoWithCallHandlerBase<TArgumentList : K
}
}
fun findElementForParameterInfo(file: KtFile, offset: Int): TArgumentList? {
val token = file.findElementAt(offset) ?: return null
return PsiTreeUtil.getParentOfType(token, argumentListClass.java, true, *STOP_SEARCH_CLASSES.toTypedArray())
}
override fun findElementForUpdatingParameterInfo(context: UpdateParameterInfoContext): TArgumentList? {
val element = context.file.findElementAt(context.offset) ?: return null
return PsiTreeUtil.getParentOfType(element, argumentListClass.java)
@@ -148,10 +158,25 @@ abstract class KotlinHighLevelParameterInfoWithCallHandlerBase<TArgumentList : K
if (context.parameterOwner !== argumentList) {
context.removeHint()
}
val currentArgumentIndex = getCurrentArgumentIndex(context, argumentList)
val currentArgumentIndex = getCurrentArgumentIndex(context.offset, argumentList)
context.setCurrentParameter(currentArgumentIndex)
val callElement = argumentList.parent as? KtElement ?: return
val callInfos = createCallInfos(argumentList, currentArgumentIndex) ?: return
for ((index, objectToView) in context.objectsToView.withIndex()) {
val candidateInfo = objectToView as? CandidateInfo ?: continue
if (index >= callInfos.size) {
// Number of candidates somehow changed while UI is shown, which should NOT be possible. Bail out to be safe.
return
}
candidateInfo.callInfo = callInfos[index]
}
}
@OptIn(KaExperimentalApi::class)
fun createCallInfos(argumentList: TArgumentList, currentArgumentIndex: Int): List<CallInfo>? {
val callElement = argumentList.parent as? KtElement ?: return null
val result = mutableListOf<CallInfo>()
analyze(callElement) {
if (callElement !is KtCallElement && callElement !is KtArrayAccessExpression) return@analyze
@@ -160,14 +185,7 @@ abstract class KotlinHighLevelParameterInfoWithCallHandlerBase<TArgumentList : K
val candidates = collectCallCandidates(callElement)
val hasMultipleApplicableBestCandidates = candidates.count { candidate -> candidate.withMapping.isApplicableBestCandidate } > 1
for ((index, objectToView) in context.objectsToView.withIndex()) {
val candidateInfo = objectToView as? CandidateInfo ?: continue
if (index >= candidates.size) {
// Number of candidates somehow changed while UI is shown, which should NOT be possible. Bail out to be safe.
return
}
for ((index, objectToView) in candidates.withIndex()) {
val (candidateSignature, argumentMapping, isApplicableBestCandidate) = candidates[index].withMapping
// For array set calls, we only want the index arguments in brackets, which are all except the last (the value to set).
@@ -232,7 +250,32 @@ abstract class KotlinHighLevelParameterInfoWithCallHandlerBase<TArgumentList : K
else -> null
}
candidateInfo.callInfo = CallInfo(
val representation = CallStringRepresentation(
buildString {
candidateSignature.receiverType?.let {
append(it.render(typeRenderer, position = Variance.IN_VARIANCE))
append(".")
}
val name = when(val symbol = candidateSignature.symbol) {
is KaConstructorSymbol -> (symbol.containingDeclaration as? KaClassSymbol)?.name
else -> symbol.name
}
append(name?.render())
},
buildString {
when(val symbol = candidateSignature.symbol) {
is KaConstructorSymbol ->{}
else -> {
append(": ")
append(candidateSignature.returnType.render(typeRenderer, position = Variance.OUT_VARIANCE))
}
}
}
)
result += CallInfo(
candidateSignature.symbol.psi,
callElement,
valueArguments,
firstArgumentInNamedMode,
@@ -244,13 +287,14 @@ abstract class KotlinHighLevelParameterInfoWithCallHandlerBase<TArgumentList : K
hasTypeMismatchBeforeCurrent,
highlightParameterIndex,
isDeprecated = candidateSignature.symbol.deprecationStatus != null,
representation,
)
}
}
return result
}
protected open fun getCurrentArgumentIndex(context: UpdateParameterInfoContext, argumentList: TArgumentList): Int {
val offset = context.offset
open fun getCurrentArgumentIndex(offset: Int, argumentList: TArgumentList): Int {
return argumentList.allChildren
.takeWhile { it.startOffset < offset }
.count { it.node.elementType == KtTokens.COMMA }
@@ -284,7 +328,7 @@ abstract class KotlinHighLevelParameterInfoWithCallHandlerBase<TArgumentList : K
}
val returnType = parameter.returnType.takeUnless { it is KaErrorType } ?: parameter.symbol.returnType
append(returnType.render(KaTypeRendererForSource.WITH_SHORT_NAMES, position = Variance.INVARIANT))
append(returnType.render(typeRenderer, position = Variance.INVARIANT))
parameter.symbol.defaultValue?.let { defaultValue ->
append(" = ")
@@ -389,153 +433,205 @@ abstract class KotlinHighLevelParameterInfoWithCallHandlerBase<TArgumentList : K
if (!argumentListClass.java.isInstance(context.parameterOwner)) return false
val callInfo = itemToShow.callInfo ?: return false
val currentArgumentIndex = min(context.currentParameterIndex, callInfo.arguments.size)
if (currentArgumentIndex < 0) return false
with(callInfo) {
var highlightStartOffset = -1
var highlightEndOffset = -1
var isDisabledBeforeHighlight = false
var hasUnmappedArgument = false
var hasUnmappedArgumentBeforeCurrent = false
var lastMappedArgumentIndex = -1
var namedMode = false
val usedParameterIndices = HashSet<Int>()
val text = buildString {
var argumentIndex = 0
val parameterDelimiterIndexes = mutableListOf<Int>()
fun appendParameter(
parameterIndex: Int,
shouldHighlight: Boolean = false,
isNamed: Boolean = false,
markUsedUnusedParameterBorder: Boolean = false
) {
argumentIndex++
if (length > 0) {
append(", ")
parameterDelimiterIndexes.add(length)
if (markUsedUnusedParameterBorder) {
// This is used to "disable" the used parameters, when in "named mode" and there are more unused parameters.
// See NamedParameter3.kt test. Disabling them gives a visual cue that they are already used.
// Highlight something as bold to show text before as disabled
highlightStartOffset = length - 1
highlightEndOffset = length - 1
isDisabledBeforeHighlight = true
}
}
if (shouldHighlight) {
highlightStartOffset = length
}
val surroundInBrackets = isNamed || namedMode
if (surroundInBrackets) {
append("[")
}
append(parameterIndexToText[parameterIndex])
if (surroundInBrackets) {
append("]")
}
if (shouldHighlight) {
highlightEndOffset = length
}
}
if (valueArguments != null) {
for (valueArgument in valueArguments) {
val parameterIndex = argumentToParameterIndex[valueArgument.getArgumentExpression()]
if (valueArgument == firstArgumentInNamedMode){
namedMode = true
}
if (parameterIndex == null) {
hasUnmappedArgument = true
if (argumentIndex < currentArgumentIndex) {
hasUnmappedArgumentBeforeCurrent = true
}
argumentIndex++
continue
}
lastMappedArgumentIndex = argumentIndex
if (!usedParameterIndices.add(parameterIndex)) continue
val shouldHighlight = parameterIndex == highlightParameterIndex
appendParameter(parameterIndex, shouldHighlight, valueArgument.isNamed())
}
} else {
// This is for array get/set calls which don't have KtValueArguments.
for (argument in arguments) {
val parameterIndex = argumentToParameterIndex[argument]
if (parameterIndex == null) {
hasUnmappedArgument = true
if (argumentIndex < currentArgumentIndex) {
hasUnmappedArgumentBeforeCurrent = true
}
argumentIndex++
continue
}
lastMappedArgumentIndex = argumentIndex
if (!usedParameterIndices.add(parameterIndex)) continue
val shouldHighlight = parameterIndex == highlightParameterIndex
appendParameter(parameterIndex, shouldHighlight)
}
}
for (parameterIndex in 0 until valueParameterCount) {
if (parameterIndex !in usedParameterIndices) {
// Highlight the first unused parameter if it is in the correct position
val shouldHighlight = !namedMode && highlightStartOffset == -1
appendParameter(
parameterIndex,
shouldHighlight,
markUsedUnusedParameterBorder = namedMode && highlightStartOffset == -1
)
}
}
if (length == 0) {
append(CodeInsightBundle.message("parameter.info.no.parameters"))
} else {
val useMultilineParameters = Registry.`is`("kotlin.multiline.function.parameters.info")
if (useMultilineParameters && argumentIndex > SINGLE_LINE_PARAMETERS_COUNT) {
parameterDelimiterIndexes.forEach { offset ->
replace(offset - 1, offset, "\n")
}
val uiModel = callInfo.toUiModel(context.currentParameterIndex) ?: return false
val backgroundColor = if (callInfo.shouldHighlightGreen) GREEN_BACKGROUND else context.defaultParameterColor
val signature = uiModel.signatureModel
val text = buildString {
signature.parts.forEach { piece -> append(piece.text) }
val useMultilineParameters = Registry.`is`("kotlin.multiline.function.parameters.info", false)
if (useMultilineParameters && signature.parts.size > SINGLE_LINE_PARAMETERS_COUNT) {
signature.parts.forEachIndexed { i, piece ->
if (piece is SignaturePart.Parameter) {
val offset = signature.getRange(i).first
replace(offset - 1, offset, "\n")
}
}
}
val backgroundColor = if (shouldHighlightGreen) GREEN_BACKGROUND else context.defaultParameterColor
// Disabled when there are too many arguments.
val allParametersUsed = usedParameterIndices.size == valueParameterCount
val supportsTrailingCommas = callElement.languageVersionSettings.supportsFeature(LanguageFeature.TrailingCommas)
val afterTrailingComma = arguments.isNotEmpty() && currentArgumentIndex == arguments.size
val isInPositionToEnterArgument = !supportsTrailingCommas && afterTrailingComma
val isAfterMappedArgs = currentArgumentIndex > lastMappedArgumentIndex
val tooManyArgs = allParametersUsed && (isInPositionToEnterArgument || hasUnmappedArgument) && (isAfterMappedArgs || namedMode)
val isDisabled = tooManyArgs || hasTypeMismatchBeforeCurrent || hasUnmappedArgumentBeforeCurrent
context.setupUIComponentPresentation(
text,
highlightStartOffset,
highlightEndOffset,
isDisabled,
/*strikeout=*/ isDeprecated,
isDisabledBeforeHighlight,
backgroundColor
)
}
val highlightedRange =run {
val index= signature.parts.withIndex()
.firstOrNull { (it.value as? SignaturePart.Parameter)?.isHighlighted == true }?.index
?: return@run Pair(-1, -1)
signature.getRange(index)
}
context.setupUIComponentPresentation(
text,
highlightedRange.first,
highlightedRange.second,
uiModel.isDisabled,
/*strikeout=*/ callInfo.isDeprecated,
uiModel.isDisabledBeforeHighlight,
backgroundColor
)
return true
}
sealed interface SignaturePart {
val text: String
data class Text(override val text: String) : SignaturePart
data class Parameter(override val text: String, val isHighlighted: Boolean) : SignaturePart
}
data class UiModel(
val signatureModel: SignatureModel,
val isDisabled: Boolean,
val isDisabledBeforeHighlight: Boolean,
)
data class SignatureModel(
val parts: List<SignaturePart>,
) {
val text: String get() = parts.joinToString("") { it.text }
private val ranges by lazy {
var offset = 0
val result = mutableListOf<Pair<Int, Int>>()
for (piece in parts) {
result += Pair(offset, offset + piece.text.length)
offset += piece.text.length
}
result
}
fun getRange(index: Int): Pair<Int, Int> {
return ranges[index]
}
}
fun CallInfo.toUiModel(
currentParameterIndex: Int,
appendNoParametersMessage: Boolean = true,
): UiModel? {
val currentArgumentIndex = min(currentParameterIndex, arguments.size)
if (currentArgumentIndex < 0) return null
var wasParameterHighlighted = false
var isDisabledBeforeHighlight = false
var hasUnmappedArgument = false
var hasUnmappedArgumentBeforeCurrent = false
var lastMappedArgumentIndex = -1
var namedMode = false
val usedParameterIndices = HashSet<Int>()
val signatureParts = mutableListOf<SignaturePart>()
var argumentIndex = 0
fun appendParameter(
parameterIndex: Int,
shouldHighlight: Boolean = false,
isNamed: Boolean = false,
markUsedUnusedParameterBorder: Boolean = false
) {
argumentIndex++
if (signatureParts.isNotEmpty()) {
signatureParts.add(SignaturePart.Text(","))
if (markUsedUnusedParameterBorder) {
// This is used to "disable" the used parameters, when in "named mode" and there are more unused parameters.
// See NamedParameter3.kt test. Disabling them gives a visual cue that they are already used.
signatureParts.add(SignaturePart.Parameter("", isHighlighted = true))
isDisabledBeforeHighlight = true
wasParameterHighlighted = true
}
signatureParts.add(SignaturePart.Text(" "))
}
val surroundInBrackets = isNamed || namedMode
val parameterText = buildString {
if (surroundInBrackets) {
append("[")
}
append(parameterIndexToText[parameterIndex])
if (surroundInBrackets) {
append("]")
}
}
if (shouldHighlight) {
wasParameterHighlighted = true
}
signatureParts.add(SignaturePart.Parameter(parameterText, shouldHighlight))
}
if (valueArguments != null) {
for (valueArgument in valueArguments) {
val parameterIndex = argumentToParameterIndex[valueArgument.getArgumentExpression()]
if (valueArgument == firstArgumentInNamedMode) {
namedMode = true
}
if (parameterIndex == null) {
hasUnmappedArgument = true
if (argumentIndex < currentArgumentIndex) {
hasUnmappedArgumentBeforeCurrent = true
}
argumentIndex++
continue
}
lastMappedArgumentIndex = argumentIndex
if (!usedParameterIndices.add(parameterIndex)) continue
val shouldHighlight = parameterIndex == highlightParameterIndex
appendParameter(parameterIndex, shouldHighlight, valueArgument.isNamed())
}
} else {
// This is for array get/set calls which don't have KtValueArguments.
for (argument in arguments) {
val parameterIndex = argumentToParameterIndex[argument]
if (parameterIndex == null) {
hasUnmappedArgument = true
if (argumentIndex < currentArgumentIndex) {
hasUnmappedArgumentBeforeCurrent = true
}
argumentIndex++
continue
}
lastMappedArgumentIndex = argumentIndex
if (!usedParameterIndices.add(parameterIndex)) continue
val shouldHighlight = parameterIndex == highlightParameterIndex
appendParameter(parameterIndex, shouldHighlight)
}
}
for (parameterIndex in 0 until valueParameterCount) {
if (parameterIndex !in usedParameterIndices) {
// Highlight the first unused parameter if it is in the correct position
val shouldHighlight = !namedMode && !wasParameterHighlighted
appendParameter(
parameterIndex,
shouldHighlight,
markUsedUnusedParameterBorder = namedMode && !wasParameterHighlighted
)
}
}
if (appendNoParametersMessage && signatureParts.isEmpty()) {
signatureParts.add(SignaturePart.Text(CodeInsightBundle.message("parameter.info.no.parameters")))
}
// Disabled when there are too many arguments.
val allParametersUsed = usedParameterIndices.size == valueParameterCount
val supportsTrailingCommas = callElement.languageVersionSettings.supportsFeature(LanguageFeature.TrailingCommas)
val afterTrailingComma = arguments.isNotEmpty() && currentArgumentIndex == arguments.size
val isInPositionToEnterArgument = !supportsTrailingCommas && afterTrailingComma
val isAfterMappedArgs = currentArgumentIndex > lastMappedArgumentIndex
val tooManyArgs = allParametersUsed && (isInPositionToEnterArgument || hasUnmappedArgument) && (isAfterMappedArgs || namedMode)
val isDisabled = tooManyArgs || hasTypeMismatchBeforeCurrent || hasUnmappedArgumentBeforeCurrent
return UiModel(
SignatureModel(signatureParts),
isDisabled,
isDisabledBeforeHighlight,
)
}
data class CallInfo(
val target: PsiElement?,
val callElement: KtElement,
val valueArguments: List<KtValueArgument>?,
val firstArgumentInNamedMode: KtValueArgument?,
@@ -547,6 +643,12 @@ abstract class KotlinHighLevelParameterInfoWithCallHandlerBase<TArgumentList : K
val hasTypeMismatchBeforeCurrent: Boolean,
val highlightParameterIndex: Int?,
val isDeprecated: Boolean,
val representation: CallStringRepresentation,
)
data class CallStringRepresentation(
val beforeParameters: String,
val afterParameters: String,
)
data class CandidateInfo(