Editor/HotCodeEditor/Navigation/SignatureHelpProvider.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;

public record SignatureParameter( string Display, string Name, string Documentation );

/// <summary>
/// One overload, shown as Prefix + parameters joined with ", " + Suffix.
/// </summary>
public record SignatureInfo( string Prefix, IReadOnlyList<SignatureParameter> Parameters, string Suffix, string Documentation );

/// <summary>
/// Parameter hints for the call around the caret. <see cref="ActiveParameter"/> is -1 when the caret
/// is past the last parameter. Open paren position identifies the call, so the editor can tell
/// "same call, new argument" from "different call".
/// </summary>
public record SignatureHelpResult( IReadOnlyList<SignatureInfo> Signatures, int ActiveSignature, int ActiveParameter, int OpenParenLine, int OpenParenColumn );

/// <summary>
/// Parameter hints from Roslyn: finds the innermost argument list around the caret
/// (method call, <c>new Foo(</c>, or <c>base(</c>/<c>this(</c>) and lists its overloads.
/// </summary>
public static class SignatureHelpProvider
{
	static readonly SymbolDisplayFormat TypeFormat = SymbolDisplayFormat.MinimallyQualifiedFormat;

	/// <summary>
	/// Hints for (line, column), or null when the caret isn't inside a call's brackets.
	/// </summary>
	public static SignatureHelpResult Get( RoslynProject.Snapshot snapshot, int line, int column )
	{
		try
		{
			return GetInternal( snapshot, line, column );
		}
		catch ( Exception )
		{
			return null;
		}
	}

	private static SignatureHelpResult GetInternal( RoslynProject.Snapshot snapshot, int line, int column )
	{
		var (compilation, tree) = RoslynProject.GetCompilation( snapshot );
		if ( tree is null ) return null;

		var text = tree.GetText();
		if ( line < 0 || line >= text.Lines.Count ) return null;

		var position = Math.Min( text.Lines[line].Start + column, text.Lines[line].End );
		var root = tree.GetRoot();

		var argumentList = FindArgumentList( root, position );
		if ( argumentList is null ) return null;

		var model = compilation.GetSemanticModel( tree );
		var (methods, chosen) = Candidates( model, argumentList, position );
		if ( methods.Count == 0 ) return null;

		// Which argument is the caret in? Count the commas before it; a named argument picks its parameter by name.
		var argumentIndex = argumentList.Arguments.GetSeparators().Count( s => s.SpanStart < position );
		var currentArgument = argumentIndex < argumentList.Arguments.Count ? argumentList.Arguments[argumentIndex] : null;
		var namedParameter = currentArgument?.NameColon?.Name.Identifier.ValueText;

		var activeSignature = chosen is not null ? methods.FindIndex( m => SymbolEqualityComparer.Default.Equals( m, chosen ) ) : -1;
		if ( activeSignature < 0 )
			activeSignature = Math.Max( 0, methods.FindIndex( m => m.Parameters.Length > argumentIndex || m.Parameters.LastOrDefault()?.IsParams == true ) );

		var active = methods[activeSignature];
		var activeParameter = ActiveParameterIndex( active, argumentIndex, namedParameter );

		var open = text.Lines.GetLinePosition( argumentList.OpenParenToken.SpanStart );
		return new SignatureHelpResult( methods.Select( Describe ).ToList(), activeSignature, activeParameter, open.Line, open.Character );
	}

	/// <summary>
	/// Index of the parameter for an argument: by name for named arguments, otherwise by position,
	/// staying on a trailing "params" array; -1 when there are more arguments than parameters.
	/// </summary>
	public static int ActiveParameterIndex( IMethodSymbol method, int argumentIndex, string namedParameter )
	{
		if ( namedParameter is not null )
			return method.Parameters.IndexOf( method.Parameters.FirstOrDefault( p => p.Name == namedParameter ) );

		if ( argumentIndex < method.Parameters.Length ) return argumentIndex;
		return method.Parameters.LastOrDefault()?.IsParams == true ? method.Parameters.Length - 1 : -1;
	}

	/// <summary>
	/// The innermost argument list whose brackets contain the position. Also handles calls that
	/// haven't been closed yet ("Foo( a, |").
	/// </summary>
	private static ArgumentListSyntax FindArgumentList( SyntaxNode root, int position )
	{
		ArgumentListSyntax best = null;

		foreach ( var at in new[] { position - 1, position } )
		{
			if ( at < 0 || at >= root.FullSpan.End ) continue;

			foreach ( var list in root.FindToken( at ).Parent?.AncestorsAndSelf().OfType<ArgumentListSyntax>() ?? Enumerable.Empty<ArgumentListSyntax>() )
			{
				var afterOpen = position > list.OpenParenToken.SpanStart;
				var beforeClose = list.CloseParenToken.IsMissing || position <= list.CloseParenToken.SpanStart;
				if ( !afterOpen || !beforeClose ) continue;

				// Innermost wins: a nested call's list starts later
				if ( best is null || list.SpanStart > best.SpanStart ) best = list;
				break;
			}
		}

		return best;
	}

	private static (List<IMethodSymbol> Methods, IMethodSymbol Chosen) Candidates( SemanticModel model, ArgumentListSyntax list, int position )
	{
		var methods = new List<IMethodSymbol>();
		IMethodSymbol chosen = null;

		switch ( list.Parent )
		{
			case InvocationExpressionSyntax invocation:
				methods.AddRange( model.GetMemberGroup( invocation.Expression ).OfType<IMethodSymbol>() );
				chosen = Chosen( model.GetSymbolInfo( invocation ) );
				break;

			case BaseObjectCreationExpressionSyntax creation:
				if ( model.GetTypeInfo( creation ).Type is INamedTypeSymbol type )
					methods.AddRange( type.InstanceConstructors.Where( c => model.IsAccessible( position, c ) ) );
				chosen = Chosen( model.GetSymbolInfo( creation ) );
				break;

			case ConstructorInitializerSyntax initializer:
				var info = model.GetSymbolInfo( initializer );
				var target = (info.Symbol ?? info.CandidateSymbols.FirstOrDefault())?.ContainingType;
				if ( target is not null )
					methods.AddRange( target.InstanceConstructors.Where( c => model.IsAccessible( position, c ) ) );
				chosen = Chosen( info );
				break;
		}

		methods = methods
			.Where( m => m.MethodKind is not (MethodKind.PropertyGet or MethodKind.PropertySet) )
			.Distinct( SymbolEqualityComparer.Default ).Cast<IMethodSymbol>()
			.OrderBy( m => m.Parameters.Length )
			.ToList();

		return (methods, chosen);
	}

	private static IMethodSymbol Chosen( SymbolInfo info ) =>
		info.Symbol as IMethodSymbol ?? (info.CandidateSymbols.Length == 1 ? info.CandidateSymbols[0] as IMethodSymbol : null);

	private static SignatureInfo Describe( IMethodSymbol method )
	{
		var docs = DocComment.Parse( (method.ReducedFrom ?? method).GetDocumentationCommentXml( expandIncludes: true ) );

		string prefix;
		if ( method.MethodKind == MethodKind.Constructor )
			prefix = $"new {method.ContainingType.ToDisplayString( TypeFormat )}(";
		else
		{
			var typeArgs = method.IsGenericMethod ? $"<{string.Join( ", ", method.TypeArguments.Select( t => t.ToDisplayString( TypeFormat ) ) )}>" : "";
			prefix = $"{method.ReturnType.ToDisplayString( TypeFormat )} {method.ContainingType?.Name}.{method.Name}{typeArgs}(";
		}

		var parameters = method.Parameters.Select( p => new SignatureParameter(
			DescribeParameter( p ),
			p.Name,
			docs.Parameters.TryGetValue( p.Name, out var doc ) ? doc : "" ) ).ToList();

		return new SignatureInfo( prefix, parameters, ")", docs.Summary );
	}

	private static string DescribeParameter( IParameterSymbol p )
	{
		var modifier = p.RefKind switch
		{
			RefKind.Ref => "ref ",
			RefKind.Out => "out ",
			RefKind.In => "in ",
			_ => ""
		};

		var text = $"{(p.IsParams ? "params " : "")}{modifier}{p.Type.ToDisplayString( TypeFormat )} {p.Name}";

		if ( p.HasExplicitDefaultValue )
		{
			var value = p.ExplicitDefaultValue switch
			{
				null => p.Type.IsValueType ? "default" : "null",
				string s => $"\"{s}\"",
				bool b => b ? "true" : "false",
				float f => $"{f}f",
				_ => p.ExplicitDefaultValue.ToString()
			};
			text += $" = {value}";
		}

		return text;
	}
}