From 2ff9478864abca032129dd47900a31cf31b0cfbf Mon Sep 17 00:00:00 2001 From: Ilya Kirillov Date: Fri, 4 Jul 2025 16:16:52 +0200 Subject: [PATCH] [lsp] implement signature help for Kotlin ^LSP-171 fixed GitOrigin-RevId: 1b8eda93c1cc33224ff8aa501cfc7f3d26637f04 --- .../com/jetbrains/lsp/protocol/Completion.kt | 11 +- .../lsp/protocol/ServerCapabilities.kt | 1 - .../jetbrains/lsp/protocol/SignatureHelp.kt | 296 +++++++++++++ ...inHighLevelFunctionParameterInfoHandler.kt | 416 +++++++++++------- 4 files changed, 564 insertions(+), 160 deletions(-) create mode 100644 fleet/lsp.protocol/src/main/com/jetbrains/lsp/protocol/SignatureHelp.kt diff --git a/fleet/lsp.protocol/src/main/com/jetbrains/lsp/protocol/Completion.kt b/fleet/lsp.protocol/src/main/com/jetbrains/lsp/protocol/Completion.kt index 863b1167d0e4..abf89cf56990 100644 --- a/fleet/lsp.protocol/src/main/com/jetbrains/lsp/protocol/Completion.kt +++ b/fleet/lsp.protocol/src/main/com/jetbrains/lsp/protocol/Completion.kt @@ -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( diff --git a/fleet/lsp.protocol/src/main/com/jetbrains/lsp/protocol/ServerCapabilities.kt b/fleet/lsp.protocol/src/main/com/jetbrains/lsp/protocol/ServerCapabilities.kt index d9d3f8c95593..9b349aa15e6c 100644 --- a/fleet/lsp.protocol/src/main/com/jetbrains/lsp/protocol/ServerCapabilities.kt +++ b/fleet/lsp.protocol/src/main/com/jetbrains/lsp/protocol/ServerCapabilities.kt @@ -244,7 +244,6 @@ data class ServerCapabilities( typealias HoverOptions = Unknown -typealias SignatureHelpOptions = Unknown typealias TypeDefinitionOptions = Unknown typealias ImplementationOptions = Unknown typealias DocumentHighlightOptions = Unknown diff --git a/fleet/lsp.protocol/src/main/com/jetbrains/lsp/protocol/SignatureHelp.kt b/fleet/lsp.protocol/src/main/com/jetbrains/lsp/protocol/SignatureHelp.kt new file mode 100644 index 000000000000..8674362a8405 --- /dev/null +++ b/fleet/lsp.protocol/src/main/com/jetbrains/lsp/protocol/SignatureHelp.kt @@ -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, + /** + * 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, + 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( + 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, + + /** + * 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?, + + /** + * 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 { + private val listSerializer = ListSerializer(Int.serializer()) + + override val descriptor: SerialDescriptor = listSerialDescriptor() + + 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