From e08d993da3357f978c46943ccbab9554d304f09a Mon Sep 17 00:00:00 2001 From: i582 <51853996+i582@users.noreply.github.com> Date: Sat, 8 Feb 2025 21:10:49 +0400 Subject: [PATCH] feat(signature-help): add signature help for struct/message fields Fixes #61 --- server/src/server.ts | 86 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 83 insertions(+), 3 deletions(-) diff --git a/server/src/server.ts b/server/src/server.ts index 960cf517..661b500e 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -30,7 +30,7 @@ import {KeywordsCompletionProvider} from "./completion/providers/KeywordsComplet import {CompletionProvider} from "./completion/CompletionProvider" import {SelfCompletionProvider} from "./completion/providers/SelfCompletionProvider" import {ReturnCompletionProvider} from "./completion/providers/ReturnCompletionProvider" -import {BaseTy} from "./types/BaseTy" +import {BaseTy, FieldsOwnerTy} from "./types/BaseTy" import {PrepareRenameResult} from "vscode-languageserver-protocol/lib/common/protocol" import {Constant, Contract, Field, Fun, Message, Primitive, Struct, Trait} from "@server/psi/Decls" import {ReferenceCompletionProvider} from "./completion/providers/ReferenceCompletionProvider" @@ -41,7 +41,7 @@ import {MessageMethodCompletionProvider} from "./completion/providers/MessageMet import {MemberFunctionCompletionProvider} from "./completion/providers/MemberFunctionCompletionProvider" import {TopLevelFunctionCompletionProvider} from "./completion/providers/TopLevelFunctionCompletionProvider" import {measureTime, parentOfType} from "@server/psi/utils" -import {FileChangeType} from "vscode-languageserver" +import {FileChangeType, ParameterInformation} from "vscode-languageserver" import {Logger} from "@server/utils/logger" import {MapTypeCompletionProvider} from "./completion/providers/MapTypeCompletionProvider" import {UnusedParameterInspection} from "./inspections/UnusedParameterInspection" @@ -866,6 +866,8 @@ connection.onInitialize(async (params: lsp.InitializeParams): Promise<lsp.Initia parametersInfo: lsp.ParameterInformation[] presentation: string isMethod: boolean + isStructField: boolean + structFieldIndex: number } | null => { const findParametersNode = (element: NamedNode): SyntaxNode | null => { if (element instanceof Contract) { @@ -880,9 +882,64 @@ connection.onInitialize(async (params: lsp.InitializeParams): Promise<lsp.Initia "static_call_expression", "method_call_expression", "initOf", + "instance_expression", + "instance_argument", + "instance_argument_list", ) if (!callNode) return null + if (callNode.type === "instance_expression") return null // don't show any signature helps + + if (callNode.type === "instance_argument_list" || callNode.type === "instance_argument") { + let name = callNode.childForFieldName("name") + ? callNode.childForFieldName("name") + : hoverNode.type === "instance_argument" + ? hoverNode.firstChild + : hoverNode.previousNamedSibling + + if (!name) return null + if (name.type === "instance_argument") { + name = name.firstChild + } + if (!name) return null + + const type = new Expression(name, file).type() + if (!type) return null + + const instanceExpression = parentOfType(callNode, "instance_expression") + if (!instanceExpression) return null + + const instanceName = instanceExpression.childForFieldName("name") + if (!instanceName) return null + + const instanceType = new Expression(instanceName, file).type() + if (!instanceType) return null + if (!(instanceType instanceof FieldsOwnerTy)) return null + + const fields = instanceType.fields() + const fieldPresentations = fields.map( + field => `${field.name()}: ${field.typeNode()?.node.text ?? ""}`, + ) + + const fieldsInfo = fieldPresentations.map( + name => + ({ + label: name, + }) as ParameterInformation, + ) + + const presentation = instanceType.name() + "{ " + fieldPresentations.join(", ") + " }" + + return { + rawArguments: [], + parametersInfo: fieldsInfo, + presentation: presentation, + isMethod: false, + isStructField: true, + structFieldIndex: fields.findIndex(f => f.name() === name.text), + } + } + const call = new CallLike(callNode, file) const res = Reference.resolve(call.nameNode()) @@ -908,6 +965,8 @@ connection.onInitialize(async (params: lsp.InitializeParams): Promise<lsp.Initia parametersInfo, presentation: `init(${parametersString})`, isMethod: false, + isStructField: false, + structFieldIndex: 0, } } @@ -918,6 +977,8 @@ connection.onInitialize(async (params: lsp.InitializeParams): Promise<lsp.Initia parametersInfo, presentation: `fun ${call.name()}(${parametersString})`, isMethod: callNode.type === "method_call_expression" && res.withSelf(), + isStructField: false, + structFieldIndex: 0, } } @@ -932,7 +993,26 @@ connection.onInitialize(async (params: lsp.InitializeParams): Promise<lsp.Initia const res = findSignatureHelpTarget(hoverNode, file) if (!res) return null - const {parametersInfo, rawArguments, isMethod, presentation} = res + const { + parametersInfo, + rawArguments, + isMethod, + presentation, + isStructField, + structFieldIndex, + } = res + + if (isStructField) { + return { + signatures: [ + { + label: presentation, + parameters: parametersInfo, + activeParameter: structFieldIndex, + }, + ], + } + } // The algorithm below uses the positions of commas and parentheses to findTo find the active parameter, it is enough to find the last comma, which has a position in the line less than the cursor position. In order not to complicate the algorithm, we consider the opening bracket as a kind of comma for the zero element. If the cursor position is greater than the position of any comma, then we consider that this is the last element. the active parameter. //