public class BicepSignatureHelpHandler()

in src/Bicep.LangServer/Handlers/BicepSignatureHelpHandler.cs [22:378]


    public class BicepSignatureHelpHandler(ICompilationManager compilationManager, DocumentSelectorFactory documentSelectorFactory) : SignatureHelpHandlerBase
    {
        private const string FunctionArgumentStart = "(";
        private const string FunctionArgumentEnd = ")";
        private const string TypeArgumentsStart = "<";
        private const string TypeArgumentsEnd = ">";

        public override Task<SignatureHelp?> Handle(SignatureHelpParams request, CancellationToken cancellationToken)
        {
            // local function

            CompilationContext? context = compilationManager.GetCompilation(request.TextDocument.Uri);
            if (context == null)
            {
                return NoHelp();
            }

            int offset = PositionHelper.GetOffset(context.LineStarts, request.Position);

            return GetActiveSyntaxInNeedOfSignatureHelp(context.ProgramSyntax, offset) switch
            {
                FunctionCallSyntaxBase functionCall
                    => Handle(context, functionCall, offset, request),
                ParameterizedTypeInstantiationSyntaxBase typeInstantiation
                    => Handle(context, typeInstantiation, offset),
                _ => NoHelp(),
            };
        }

        private static Task<SignatureHelp?> NoHelp() => Task.FromResult<SignatureHelp?>(null);

        private static SyntaxBase? GetActiveSyntaxInNeedOfSignatureHelp(ProgramSyntax syntax, int offset)
        {
            // if the cursor is placed after the closing paren of a function, it needs to count as outside of that function call
            // for purposes of signature help (otherwise we'll show the wrong function when function calls are nested)
            var matchingNodes = SyntaxMatcher.FindNodesMatchingOffsetExclusive(syntax, offset);

            var functionCallIndex = matchingNodes
                .FindLastIndex(
                    matchingNodes.Count - 1,
                    current => current is FunctionCallSyntaxBase functionCall && TextSpan.BetweenExclusive(functionCall.OpenParen.Span, functionCall.CloseParen).ContainsInclusive(offset));

            if (functionCallIndex >= 0)
            {
                return matchingNodes[functionCallIndex];
            }

            var parameterizedTypeInstantiationIndex = matchingNodes
                .FindLastIndex(
                    matchingNodes.Count - 1,
                    current => current is ParameterizedTypeInstantiationSyntaxBase typeInstantiation &&
                        TextSpan.BetweenExclusive(typeInstantiation.OpenChevron.Span, typeInstantiation.CloseChevron).ContainsInclusive(offset));

            if (parameterizedTypeInstantiationIndex >= 0)
            {
                return matchingNodes[parameterizedTypeInstantiationIndex];
            }

            return null;
        }

        private static Task<SignatureHelp?> Handle(CompilationContext context,
            FunctionCallSyntaxBase functionCall,
            int offset,
            SignatureHelpParams request)
        {
            var semanticModel = context.Compilation.GetEntrypointSemanticModel();
            var symbol = semanticModel.GetSymbolInfo(functionCall);
            if (symbol is not IFunctionSymbol functionSymbol)
            {
                // no symbol or symbol is not a function
                return NoHelp();
            }

            // suppress ErrorType in arguments because the code is being written
            // this prevents function signature mismatches due to errors
            var normalizedArgumentTypes = NormalizeArgumentTypes(functionCall.Arguments, semanticModel);

            // do not include return type in signatures for decorator functions
            // because the return type on decorators is currently an internal implementation detail
            // which will be confusing to users
            // (can revisit if we add decorator extensibility in the future)
            var includeReturnType = semanticModel.Binder.GetParent(functionCall) is not DecoratorSyntax;

            var signatureHelp = CreateSignatureHelp(functionCall.Arguments, normalizedArgumentTypes, functionSymbol, offset, includeReturnType);
            signatureHelp = TryReuseActiveSignature(request.Context, signatureHelp);

            return Task.FromResult<SignatureHelp?>(signatureHelp);
        }

        private static SignatureHelp TryReuseActiveSignature(SignatureHelpContext? context, SignatureHelp signatureHelp)
        {
            if (context?.ActiveSignatureHelp == null ||
                string.Equals(context.TriggerCharacter, FunctionArgumentStart, StringComparison.Ordinal) ||
                string.Equals(context.TriggerCharacter, FunctionArgumentEnd, StringComparison.Ordinal))
            {
                // we don't have a previous active signature or the user typed ( or ), which would indicate a new "session"
                return signatureHelp;
            }

            if (CheckIfSignatureHelpSimilar(context.ActiveSignatureHelp, signatureHelp))
            {
                // the signature help is for the same function so we can reuse the active signature index
                // this prevents resetting of the active signature when multiple overloads are ambiguous and the user selected a specific one manually
                return signatureHelp with
                {
                    ActiveSignature = context.ActiveSignatureHelp.ActiveSignature
                };
            }

            // cannot improve the active signature - return as-is
            return signatureHelp;
        }

        private static bool CheckIfSignatureHelpSimilar(SignatureHelp active, SignatureHelp @new)
        {
            // local function
            static string GetFunctionName(SignatureInformation info)
            {
                var openParenIndex = info.Label.IndexOf(FunctionArgumentStart, StringComparison.Ordinal);
                return openParenIndex <= 0 ? info.Label : info.Label.Substring(0, openParenIndex - 1);
            }

            var newSignatureCount = @new.Signatures.Count();
            if (active.ActiveSignature > newSignatureCount || active.Signatures.Count() != newSignatureCount)
            {
                return false;
            }

            return active.Signatures
                .Zip(@new.Signatures)
                .All(tuple => string.Equals(GetFunctionName(tuple.First), GetFunctionName(tuple.Second), StringComparison.Ordinal) &&
                              string.Equals(tuple.First.Documentation?.MarkupContent?.Value, tuple.Second.Documentation?.MarkupContent?.Value, StringComparison.Ordinal));
        }

        private static List<TypeSymbol> NormalizeArgumentTypes(ImmutableArray<FunctionArgumentSyntax> arguments, SemanticModel semanticModel)
        {
            return arguments
                .Select(arg =>
                {
                    var argumentType = semanticModel.GetTypeInfo(arg);
                    return argumentType is ErrorType ? LanguageConstants.Any : argumentType;
                })
                .ToList();
        }

        private static SignatureHelp CreateSignatureHelp(ImmutableArray<FunctionArgumentSyntax> arguments, List<TypeSymbol> normalizedArgumentTypes, IFunctionSymbol symbol, int offset, bool includeReturnType)
        {
            // exclude overloads where the specified arguments have exceeded the maximum
            // allow count mismatches because the user may not have started typing the arguments yet
            var matchingOverloads = symbol.Overloads
                .Where(fo => !fo.MaximumArgumentCount.HasValue || normalizedArgumentTypes.Count <= fo.MaximumArgumentCount.Value)
                .Select(overload => (overload, result: overload.Match(normalizedArgumentTypes, out _, out _)))
                .ToList();

            int activeSignatureIndex = matchingOverloads.IndexOf(tuple => tuple.result == FunctionMatchResult.Match);
            if (activeSignatureIndex < 0)
            {
                // no best match - try potential match
                activeSignatureIndex = matchingOverloads.IndexOf(tuple => tuple.result == FunctionMatchResult.PotentialMatch);
            }

            return new SignatureHelp
            {
                Signatures = new Container<SignatureInformation>(matchingOverloads.Select(tuple => CreateSignature(tuple.overload, arguments, includeReturnType))),
                ActiveSignature = activeSignatureIndex < 0 ? (int?)null : activeSignatureIndex,
                ActiveParameter = GetActiveParameterIndex(arguments, offset)
            };
        }

        private static int? GetActiveParameterIndex(ImmutableArray<FunctionArgumentSyntax> arguments, int offset)
        {
            for (int i = 0; i < arguments.Length; i++)
            {
                // the comma token is included in the argument node, so we need to check the span of the expression
                if (arguments[i].Expression.Span.ContainsInclusive(offset))
                {
                    return i;
                }
            }

            return null;
        }

        private static SignatureInformation CreateSignature(FunctionOverload overload, ImmutableArray<FunctionArgumentSyntax> arguments, bool includeReturnType)
        {
            const string delimiter = ", ";

            var typeSignature = new StringBuilder();
            var parameters = new List<ParameterInformation>();

            typeSignature.Append(overload.Name);
            typeSignature.Append(FunctionArgumentStart);

            foreach (var fixedParameter in overload.FixedParameters)
            {
                AppendParameter(typeSignature, parameters, fixedParameter.Signature, fixedParameter.Description);
                typeSignature.Append(delimiter);
            }

            if (overload.VariableParameter != null)
            {
                // the function supports varargs
                int index = 0;

                // include minimum number of variable parameters in the signature and dynamically generate the additional ones
                while (index < overload.VariableParameter.MinimumCount || arguments.Length > parameters.Count)
                {
                    // we have a parameter that isn't accounted for in the signature
                    AppendParameter(typeSignature, parameters, overload.VariableParameter.GetNamedSignature(index), overload.VariableParameter.Description);
                    ++index;

                    typeSignature.Append(delimiter);
                }

                // on functions with varargs, we don't know if the user finished typing yet or not
                // as a result, we need to offer a hint that more arguments can be added
                // (otherwise you end up with signature help that prints something like concat() which is not helpful)
                AppendParameter(typeSignature, parameters, overload.VariableParameter.GenericSignature, overload.VariableParameter.Description);
                typeSignature.Append(delimiter);
            }

            if (parameters.Any())
            {
                // some parameters were appended, which left a trailing delimiter
                // remove the delimiter
                typeSignature.Remove(typeSignature.Length - delimiter.Length, delimiter.Length);
            }

            typeSignature.Append(FunctionArgumentEnd);

            if (includeReturnType)
            {
                typeSignature.Append(": ");
                typeSignature.Append(overload.TypeSignatureSymbol);
            }

            return new SignatureInformation
            {
                Label = typeSignature.ToString(),
                Documentation = new MarkupContent { Kind = MarkupKind.Markdown, Value = overload.Description },
                Parameters = new Container<ParameterInformation>(parameters)
            };
        }

        private static void AppendParameter(StringBuilder typeSignature, List<ParameterInformation> parameterInfos, string parameterSignature, string documentation)
        {
            int start = typeSignature.Length;
            typeSignature.Append(parameterSignature);
            int end = typeSignature.Length;

            parameterInfos.Add(new ParameterInformation
            {
                Label = new ParameterInformationLabel((start, end)),
                Documentation = new MarkupContent { Kind = MarkupKind.Markdown, Value = documentation }
            });
        }

        private static Task<SignatureHelp?> Handle(CompilationContext context,
            ParameterizedTypeInstantiationSyntaxBase typeInstantiation,
            int offset)
        {
            var semanticModel = context.Compilation.GetEntrypointSemanticModel();
            var symbol = semanticModel.GetSymbolInfo(typeInstantiation);
            if (GetSymbolType(symbol) is not TypeTemplate parameterizable)
            {
                // no symbol or symbol type is not parameterizable
                return NoHelp();
            }

            var documentation = symbol switch
            {
                AmbientTypeSymbol ambientType => ambientType.Description,
                _ => null,
            };

            return Task.FromResult<SignatureHelp?>(CreateSignatureHelp(parameterizable, documentation, typeInstantiation.Arguments, offset));
        }

        private static TypeSymbol? GetSymbolType(Symbol? symbol) => symbol switch
        {
            ITypeReference typeReference => typeReference.Type,
            DeclaredSymbol declared => declared.Type,
            PropertySymbol property => property.Type,
            _ => null,
        };

        private static SignatureHelp CreateSignatureHelp(TypeTemplate typeTemplate, string? documentation, ImmutableArray<ParameterizedTypeArgumentSyntax> arguments, int offset)
        {
            return new SignatureHelp
            {
                Signatures = new Container<SignatureInformation>(CreateSignature(typeTemplate, documentation)),
                ActiveSignature = 0,
                ActiveParameter = GetActiveParameterIndex(arguments, offset)
            };
        }

        private static int? GetActiveParameterIndex(ImmutableArray<ParameterizedTypeArgumentSyntax> arguments, int offset)
        {
            for (int i = 0; i < arguments.Length; i++)
            {
                // the comma token is included in the argument node, so we need to check the span of the expression
                if (arguments[i].Expression.Span.ContainsInclusive(offset))
                {
                    return i;
                }
            }

            return null;
        }

        private static SignatureInformation CreateSignature(TypeTemplate typeTemplate, string? documentation)
        {
            const string delimiter = ", ";

            var typeSignature = new StringBuilder();
            var parameters = new List<ParameterInformation>();

            typeSignature.Append(typeTemplate.UnparameterizedName);
            typeSignature.Append(TypeArgumentsStart);

            for (int i = 0; i < typeTemplate.Parameters.Length; i++)
            {
                if (i > 0)
                {
                    typeSignature.Append(delimiter);
                }

                AppendParameter(typeSignature, parameters, typeTemplate.Parameters[i].Signature, typeTemplate.Parameters[i].Description);
            }

            typeSignature.Append(TypeArgumentsEnd);

            return new SignatureInformation
            {
                Label = typeSignature.ToString(),
                Documentation = documentation is not null
                    ? new MarkupContent { Kind = MarkupKind.Markdown, Value = documentation }
                    : null,
                Parameters = new Container<ParameterInformation>(parameters)
            };
        }

        protected override SignatureHelpRegistrationOptions CreateRegistrationOptions(SignatureHelpCapability capability, ClientCapabilities clientCapabilities) => new()
        {
            DocumentSelector = documentSelectorFactory.CreateForBicepAndParams(),
            /*
             * ( - triggers sig. help when starting function arguments
             * , - separates function arguments
             * ) - triggers sig. help for the outer function (or nothing)
             * < - triggers sig. help when starting type parameterization arguments
             * > - triggers sig. help for the outer parameterized type (or nothing)
             */
            TriggerCharacters = new Container<string>(FunctionArgumentStart, ",", FunctionArgumentEnd, TypeArgumentsStart, TypeArgumentsEnd),
            RetriggerCharacters = new Container<string>()
        };
    }