C# Roslyn CompletionService where to get method overload information

Viewed 559

I've got a method that returning back from CompletionService.GetDescriptionAsync(Document, CompletionItem) gives me the following description:

void SQL.GetSQLiteDB(string url) (+ 1 overload)

This is a method I made on a Xamarin project, here are both method signatures:

public static void GetSQLiteDB(string url);
public static string GetSQLiteDB(string url, string name);

What's the Roslyn way to get information on both?

Here's how I'm setting up completions:

    async Task InitCodeCompletion()
    {
        host = MefHostServices.Create(MefHostServices.DefaultAssemblies);
        workspace = new AdhocWorkspace(host);

        Type[] types =
        {
            typeof(object),
            typeof(System.Linq.Enumerable),
            typeof(System.Collections.IEnumerable),
            typeof(Console),
            typeof(System.Reflection.Assembly),
            typeof(List<>),
            typeof(Type),
            typeof(SQL)
        };

        imports = types.Select(x => x.Namespace).Distinct().ToImmutableArray();
        assemblies = types.Select(x => x.Assembly).Distinct().ToImmutableArray();
        references = assemblies.Select(t => MetadataReference.CreateFromFile(t.Location) as MetadataReference).ToImmutableArray();

        compilationOptions = new CSharpCompilationOptions(
           OutputKind.DynamicallyLinkedLibrary,
           usings: imports);

        projectInfo = ProjectInfo.Create(ProjectId.CreateNewId(), VersionStamp.Create(), "Script", "Script", LanguageNames.CSharp, isSubmission: true)
           .WithMetadataReferences(references).WithCompilationOptions(compilationOptions);
        project = workspace.AddProject(projectInfo);
        documentInfo = DocumentInfo.Create(DocumentId.CreateNewId(project.Id), "Script", sourceCodeKind: SourceCodeKind.Script,
            loader: TextLoader.From(TextAndVersion.Create(SourceText.From(""), VersionStamp.Create())));
        document = workspace.AddDocument(documentInfo);

        var services = workspace.Services;

        completionService = CompletionService.GetService(document);
    }



    async Task<CodeCompletionResults> GetCompletions(string code)
    {
        string codeModified = "using SQL = XamTestNET5.Services.SQLiteGeneratorService; " + Environment.NewLine;
        codeModified += "using HtmlSvc = XamTestNET5.Services.HtmlRetrievalService;" + Environment.NewLine;
        // ^^^ The above two lines set up some simple namespace aliases in my project, if you know how to put this in a separate project document and use it in code completion please let me know in comments as otherwise doing so gives me an exception that you can't have multiple syntax trees
        codeModified += code;
        var source = SourceText.From(codeModified);
        document = document.WithText(source);

        // cursor position is at the end
        var position = source.Length;

        var completions = await completionService.GetCompletionsAsync(document, position);
        return new CodeCompletionResults() { InputCode = code, ModifiedCode = codeModified, Completions = completions }; 
    }

Here's how I'm getting them now and putting them in a browser control:

    private async void CSharpShellEnvironment_EntryCodeCompletionEntry(object sender, CSharpShellEnvironment.EntryEventArgs e)
    {
        if (e.Value != "")
        {
            CodeCompletionResults results = await GetCompletions(e.Value);
            CompletionList list = results.Completions;

            if (list != null)
            {
                if (list.Items != null)
                {
                    StringBuilder sb = new StringBuilder();
                    foreach (var item in list.Items)
                    {
                        string spanText = (item.Span.Start != item.Span.End) ? results.ModifiedCode.Substring(item.Span.Start, item.Span.Length) : "";
                        bool recommended = spanText == "" ? true : item.DisplayText.StartsWith(spanText);
                        if (recommended)
                        {
                            string fText = item.DisplayText.Substring(spanText.Length);
                            string props = "";
                            foreach(var p in item.Properties)
                            {
                                props += $"<span data-key=\"{p.Key}\" data-value=\"{p.Value}\"></span>";
                            }
                            string tags = "";
                            foreach(var t in item.Tags)
                            {
                                tags += $"<span data-tag=\"{t}\"></span>";
                            }
                            string descStr = "";
                            if (item.Tags != null)
                            {
                                if (item.Tags.Where(x => x.ToLower() == "method").FirstOrDefault() != null && item.Tags.Where(x => x.ToLower() == "public").FirstOrDefault() != null)
                                {
                                    var desc = await completionService.GetDescriptionAsync(document, item);
                                    descStr += $"<span data-desc=\"{desc.Text}\">";
                                    foreach(var part in desc.TaggedParts)
                                    {
                                        descStr += $"<span data-desc-part-tag=\"{part.Tag}\" data-desc-part-text=\"{part.Text}\"></span>";
                                    }
                                    descStr += "</span>";
                                }
                            }
                            sb.AppendLine($"<div class=\"codecompleteentry\" data-display-text=\"{item.DisplayText}\" data-span-text=\"{spanText}\" data-final-text=\"{fText}\">{props}{tags}{descStr}{fText}</div>");
                        }
                    }
                    string scriptInputClick = "Array.prototype.forEach.call(document.getElementsByClassName('codecompleteentry'), function(el) { el.addEventListener('click', function(elem) { var text = { MessageType: 'CodeCompletion', Parameters: JSON.stringify({ DataDisplayText: el.getAttribute('data-display-text'), DataSpanText: el.getAttribute('data-span-text'), DataFinalText: el.getAttribute('data-final-text') }), Message: el.innerText }; window.chrome.webview.postMessage(text); } ); });";
                    sb.AppendLine($"<script type=\"text/javascript\">{scriptInputClick}</script>");
                    env.EnterCodeCompletionResponse(sb.ToString());
                }
                else
                {
                    env.EnterCodeCompletionResponse(strNoSuggestions);
                }
            }
            else
            {
                env.EnterCodeCompletionResponse(strNoSuggestions);
            }
        }
        else
        {
            env.EnterCodeCompletionResponse(strNoSuggestions);
        }
    }
1 Answers

It seems on the surface that CompletionSurface has everything you need, but it doesn't, you need to reference the Document's SemanticModel in order to get all of the signature overloads of a method when the user types ( on a method during code completion.

It wasn't very obvious to me until I started looking through the RoslynPad source, which I recommend doing for a practical example: https://github.com/aelij/RoslynPad

    List<IEnumerable<ReferencedSymbol>> allMethodRefs = new List<IEnumerable<ReferencedSymbol>>();

    async Task<CodeCompletionResults> GetCompletions(string code)
    {
        string codeModified = "using SQL = XamTestNET5.Services.SQLiteGeneratorService; " + Environment.NewLine;
        codeModified += "using HtmlSvc = XamTestNET5.Services.HtmlRetrievalService;" + Environment.NewLine;
        // ^^^ I put my namespace aliases in the same SyntaxTree for now,
        //     I'd like a better solution though.
        codeModified += code;
        var source = SourceText.From(codeModified);
        document = document.WithText(source);

        // cursor position is at the end
        var position = source.Length;

        var completions = await completionService.GetCompletionsAsync(document, position);
        syntaxRoot = await document.GetSyntaxRootAsync();
        semanticModel = await document.GetSemanticModelAsync();
        var methods = syntaxRoot.DescendantNodes().OfType<InvocationExpressionSyntax>();

        allMethodRefs = new List<IEnumerable<ReferencedSymbol>>();

        if (methods != null)
        {
            if (methods.Count() > 0)
            {
                foreach(var m in methods)
                {
                    var info = semanticModel.GetSymbolInfo(m);
                    if (info.Symbol != null)
                    {
                        allMethodRefs.Add(await SymbolFinder.FindReferencesAsync(info.Symbol, solution));
                    }
                    else
                    {
                        foreach(var symbol in info.CandidateSymbols)
                        {
                            allMethodRefs.Add(await SymbolFinder.FindReferencesAsync(symbol, solution));
                        }
                    }
                }
            }
        }
        return new CodeCompletionResults() { InputCode = code, ModifiedCode = codeModified, Completions = completions }; 
    }
Related