File: Completion\CompletionResolveTests.cs
Web Access
Project: src\src\LanguageServer\ProtocolUnitTests\Microsoft.CodeAnalysis.LanguageServer.Protocol.UnitTests.csproj (Microsoft.CodeAnalysis.LanguageServer.Protocol.UnitTests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
 
#nullable disable
 
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Completion;
using Microsoft.CodeAnalysis.Extensions;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.LanguageServer.Handler.Completion;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.Shared.TestHooks;
using Microsoft.CodeAnalysis.Text;
using Microsoft.VisualStudio.Text.Adornments;
using Roslyn.LanguageServer.Protocol;
using Roslyn.Test.Utilities;
using Xunit;
using Xunit.Abstractions;
using LSP = Roslyn.LanguageServer.Protocol;
 
namespace Microsoft.CodeAnalysis.LanguageServer.UnitTests.Completion
{
    public class CompletionResolveTests : AbstractLanguageServerProtocolTests
    {
        public CompletionResolveTests(ITestOutputHelper testOutputHelper) : base(testOutputHelper)
        {
        }
 
        [Theory, CombinatorialData]
        public async Task TestResolveCompletionItemFromListAsync(bool mutatingLspWorkspace)
        {
            var markup =
@"class A
{
    void M()
    {
        {|caret:|}
    }
}";
 
            var clientCapabilities = new LSP.VSInternalClientCapabilities
            {
                SupportsVisualStudioExtensions = true,
                TextDocument = new TextDocumentClientCapabilities()
                {
                    Completion = new VSInternalCompletionSetting()
                    {
                        CompletionList = new VSInternalCompletionListSetting()
                        {
                            Data = true,
                        }
                    }
                }
            };
            await using var testLspServer = await CreateTestLspServerAsync(markup, mutatingLspWorkspace, clientCapabilities);
 
            var clientCompletionItem = await GetCompletionItemToResolveAsync<LSP.VSInternalCompletionItem>(
                testLspServer,
                label: "A").ConfigureAwait(false);
 
            var description = new ClassifiedTextElement(CreateClassifiedTextRunForClass("A"));
            var expected = CreateResolvedCompletionItem(clientCompletionItem, description, null);
 
            var results = (LSP.VSInternalCompletionItem)await RunResolveCompletionItemAsync(
                testLspServer, clientCompletionItem).ConfigureAwait(false);
            AssertJsonEquals(expected, results);
        }
 
        [Theory, CombinatorialData]
        public async Task TestResolveCompletionItemAsync(bool mutatingLspWorkspace)
        {
            var markup =
@"class A
{
    void M()
    {
        {|caret:|}
    }
}";
            await using var testLspServer = await CreateTestLspServerAsync(markup, mutatingLspWorkspace, new LSP.VSInternalClientCapabilities { SupportsVisualStudioExtensions = true });
            var clientCompletionItem = await GetCompletionItemToResolveAsync<LSP.VSInternalCompletionItem>(testLspServer, label: "A").ConfigureAwait(false);
 
            var description = new ClassifiedTextElement(CreateClassifiedTextRunForClass("A"));
            var expected = CreateResolvedCompletionItem(clientCompletionItem, description, null);
 
            var results = (LSP.VSInternalCompletionItem)await RunResolveCompletionItemAsync(
                testLspServer, clientCompletionItem).ConfigureAwait(false);
            AssertJsonEquals(expected, results);
        }
 
        [Theory, CombinatorialData]
        public async Task TestResolveOverridesCompletionItemAsync(bool mutatingLspWorkspace)
        {
            var markup =
@"abstract class A
{
    public abstract void M();
}
 
class B : A
{
    override {|caret:|}
}";
            await using var testLspServer = await CreateTestLspServerAsync(markup, mutatingLspWorkspace, new LSP.VSInternalClientCapabilities { SupportsVisualStudioExtensions = true });
            var clientCompletionItem = await GetCompletionItemToResolveAsync<LSP.VSInternalCompletionItem>(testLspServer, label: "M()").ConfigureAwait(false);
            var results = (LSP.VSInternalCompletionItem)await RunResolveCompletionItemAsync(
                testLspServer, clientCompletionItem).ConfigureAwait(false);
 
            Assert.NotNull(results.TextEdit);
            Assert.Null(results.InsertText);
            Assert.Equal(@"public override void M()
    {
        throw new System.NotImplementedException();
    }", results.TextEdit.Value.First.NewText);
        }
 
        [Theory, CombinatorialData, WorkItem("https://github.com/dotnet/roslyn/issues/51125")]
        public async Task TestResolveOverridesCompletionItem_SnippetsEnabledAsync(bool mutatingLspWorkspace)
        {
            var markup =
@"abstract class A
{
    public abstract void M();
}
 
class B : A
{
    override {|caret:|}
}";
 
            // Explicitly enable snippets. This allows us to set the cursor with $0. Currently only applies to C# in Razor docs.
            var clientCapabilities = new LSP.VSInternalClientCapabilities
            {
                SupportsVisualStudioExtensions = true,
                TextDocument = new LSP.TextDocumentClientCapabilities
                {
                    Completion = new CompletionSetting
                    {
                        CompletionItem = new CompletionItemSetting
                        {
                            SnippetSupport = true
                        }
                    }
                }
            };
            await using var testLspServer = await CreateTestLspServerAsync(markup, mutatingLspWorkspace, clientCapabilities);
            var clientCompletionItem = await GetCompletionItemToResolveAsync<LSP.VSInternalCompletionItem>(
                testLspServer,
                label: "M()").ConfigureAwait(false);
 
            var results = (LSP.VSInternalCompletionItem)await RunResolveCompletionItemAsync(
                testLspServer, clientCompletionItem).ConfigureAwait(false);
 
            Assert.NotNull(results.TextEdit);
            Assert.Null(results.InsertText);
            Assert.Equal(@"public override void M()
    {
        throw new System.NotImplementedException();$0
    }", results.TextEdit.Value.First.NewText);
        }
 
        [Theory, CombinatorialData, WorkItem("https://github.com/dotnet/roslyn/issues/51125")]
        public async Task TestResolveOverridesCompletionItem_SnippetsEnabled_CaretOutOfSnippetScopeAsync(bool mutatingLspWorkspace)
        {
            var markup =
@"abstract class A
{
    public abstract void M();
}
 
class B : A
{
    override {|caret:|}
}";
            await using var testLspServer = await CreateTestLspServerAsync(markup, mutatingLspWorkspace);
 
            var document = testLspServer.GetCurrentSolution().Projects.First().Documents.First();
 
            var selectedItem = CodeAnalysis.Completion.CompletionItem.Create(displayText: "M", isComplexTextEdit: true);
            var (textEdit, _, _) = await CompletionResultFactory.GenerateComplexTextEditAsync(
                document, new TestCaretOutOfScopeCompletionService(testLspServer.TestWorkspace.Services.SolutionServices), selectedItem, snippetsSupported: true, insertNewPositionPlaceholder: true, CancellationToken.None).ConfigureAwait(false);
 
            Assert.Equal(@"public override void M()
    {
        throw new System.NotImplementedException();
    }", textEdit.NewText);
        }
 
        [Theory, CombinatorialData]
        public async Task TestResolveCompletionItemWithMarkupContentAsync(bool mutatingLspWorkspace)
        {
            var markup =
@"
class A
{
    /// <summary>
    /// A cref <see cref=""AMethod""/>
    /// <br/>
    /// <strong>strong text</strong>
    /// <br/>
    /// <em>italic text</em>
    /// <br/>
    /// <u>underline text</u>
    /// <para>
    /// <list type='bullet'>
    /// <item>
    /// <description>Item 1.</description>
    /// </item>
    /// <item>
    /// <description>Item 2.</description>
    /// </item>
    /// </list>
    /// <a href = ""https://google.com"" > link text</a>
    /// </para>
    /// </summary>
    void AMethod(int i)
    {
    }
 
    void M()
    {
        AMet{|caret:|}
    }
}";
            var clientCapabilities = new ClientCapabilities
            {
                TextDocument = new TextDocumentClientCapabilities
                {
                    Completion = new CompletionSetting
                    {
                        CompletionItem = new CompletionItemSetting
                        {
                            DocumentationFormat = [MarkupKind.Markdown]
                        }
                    }
                }
            };
            await using var testLspServer = await CreateTestLspServerAsync(markup, mutatingLspWorkspace, clientCapabilities);
            var clientCompletionItem = await GetCompletionItemToResolveAsync<LSP.CompletionItem>(
                testLspServer,
                label: "AMethod").ConfigureAwait(false);
 
            var expected = @"```csharp
void A.AMethod(int i)
```
  
A cref&nbsp;A\.AMethod\(int\)  
**strong text**  
_italic text_  
<u>underline text</u>  
  
•&nbsp;Item 1\.  
•&nbsp;Item 2\.  
  
[link text](https://google.com)";
 
            var results = await RunResolveCompletionItemAsync(
                testLspServer,
                clientCompletionItem).ConfigureAwait(false);
            Assert.Equal(expected, results.Documentation.Value.Second.Value);
        }
 
        [Theory, CombinatorialData]
        public async Task TestResolveCompletionItemWithPlainTextAsync(bool mutatingLspWorkspace)
        {
            var markup =
@"
class A
{
    /// <summary>
    /// A cref <see cref=""AMethod""/>
    /// <br/>
    /// <strong>strong text</strong>
    /// <br/>
    /// <em>italic text</em>
    /// <br/>
    /// <u>underline text</u>
    /// <para>
    /// <list type='bullet'>
    /// <item>
    /// <description>Item 1.</description>
    /// </item>
    /// <item>
    /// <description>Item 2.</description>
    /// </item>
    /// </list>
    /// <a href = ""https://google.com"" > link text</a>
    /// </para>
    /// </summary>
    void AMethod(int i)
    {
    }
 
    void M()
    {
        AMet{|caret:|}
    }
}";
            await using var testLspServer = await CreateTestLspServerAsync(markup, mutatingLspWorkspace);
            var clientCompletionItem = await GetCompletionItemToResolveAsync<LSP.CompletionItem>(
                testLspServer,
                label: "AMethod").ConfigureAwait(false);
 
            var expected = @"void A.AMethod(int i)
A cref A.AMethod(int)
strong text
italic text
underline text
 
• Item 1.
• Item 2.
 
link text";
 
            var results = await RunResolveCompletionItemAsync(
                testLspServer,
                clientCompletionItem).ConfigureAwait(false);
            Assert.Equal(expected, results.Documentation.Value.Second.Value);
        }
 
        [Theory, CombinatorialData]
        public async Task TestResolveCompletionItemWithPrefixSuffixAsync(bool mutatingLspWorkspace)
        {
            var markup =
@"class A
{
    void M()
    {
        var a = 10;
        a.{|caret:|}
    }
}";
            await using var testLspServer = await CreateTestLspServerAsync(markup, mutatingLspWorkspace, new LSP.VSInternalClientCapabilities { SupportsVisualStudioExtensions = true });
            var clientCompletionItem = await GetCompletionItemToResolveAsync<LSP.VSInternalCompletionItem>(testLspServer, label: "(byte)").ConfigureAwait(false);
 
            var results = (LSP.VSInternalCompletionItem)await RunResolveCompletionItemAsync(
                testLspServer, clientCompletionItem).ConfigureAwait(false);
            Assert.Equal("(byte)", results.Label);
            Assert.NotNull(results.Description);
        }
 
        [Theory, CombinatorialData]
        public async Task TestSemanticSnippetChangeAsync(bool mutatingLspWorkspace)
        {
            var markup =
                """
                using System;
                public class Program
                {
                    {|editRange:svm|}{|caret:|}
                }
                """;
            await using var testLspServer = await CreateTestLspServerAsync(markup, mutatingLspWorkspace, new LSP.VSInternalClientCapabilities { SupportsVisualStudioExtensions = true });
            testLspServer.TestWorkspace.GlobalOptions.SetGlobalOption(CompletionOptionsStorage.SnippetsBehavior, LanguageNames.CSharp, SnippetsRule.AlwaysInclude);
            testLspServer.TestWorkspace.GlobalOptions.SetGlobalOption(CompletionOptionsStorage.ShowNewSnippetExperienceUserOption, LanguageNames.CSharp, true);
 
            var clientCompletionItem = await GetCompletionItemToResolveAsync<LSP.VSInternalCompletionItem>(testLspServer, label: "svm").ConfigureAwait(false);
 
            Assert.True(clientCompletionItem.VsResolveTextEditOnCommit);
 
            var results = (LSP.VSInternalCompletionItem)await RunResolveCompletionItemAsync(
                testLspServer, clientCompletionItem).ConfigureAwait(false);
 
            Assert.NotNull(results.TextEdit);
            Assert.Null(results.InsertText);
            Assert.Equal("static void Main(string[] args)\r\n    {\r\n        \r\n    }", results.TextEdit.Value.First.NewText);
 
            var editRange = testLspServer.GetLocations("editRange").Single().Range;
            Assert.Equal(editRange, results.TextEdit.Value.First.Range);
        }
 
        private static async Task<LSP.CompletionItem> RunResolveCompletionItemAsync(TestLspServer testLspServer, LSP.CompletionItem completionItem)
        {
            return await testLspServer.ExecuteRequestAsync<LSP.CompletionItem, LSP.CompletionItem>(LSP.Methods.TextDocumentCompletionResolveName,
                           completionItem, CancellationToken.None);
        }
 
        private static VSInternalCompletionItem Clone(VSInternalCompletionItem completionItem)
        {
            return new VSInternalCompletionItem()
            {
                Label = completionItem.Label,
                LabelDetails = completionItem.LabelDetails,
                Kind = completionItem.Kind,
                Detail = completionItem.Detail,
                Documentation = completionItem.Documentation,
                Preselect = completionItem.Preselect,
                SortText = completionItem.SortText,
                FilterText = completionItem.FilterText,
                InsertText = completionItem.InsertText,
                InsertTextFormat = completionItem.InsertTextFormat,
                TextEdit = completionItem.TextEdit,
                TextEditText = completionItem.TextEditText,
                AdditionalTextEdits = completionItem.AdditionalTextEdits,
                CommitCharacters = completionItem.CommitCharacters,
                Command = completionItem.Command,
                Data = completionItem.Data,
                Icon = completionItem.Icon,
                Description = completionItem.Description,
                VsCommitCharacters = completionItem.VsCommitCharacters,
                VsResolveTextEditOnCommit = completionItem.VsResolveTextEditOnCommit,
            };
        }
 
        private static VSInternalCompletionItem CreateResolvedCompletionItem(
            VSInternalCompletionItem completionItem,
            ClassifiedTextElement description,
            string documentation)
        {
            var expectedCompletionItem = Clone(completionItem);
 
            if (documentation != null)
            {
                expectedCompletionItem.Documentation = new MarkupContent()
                {
                    Kind = LSP.MarkupKind.PlainText,
                    Value = documentation
                };
            }
 
            expectedCompletionItem.Description = description.ToLSPElement();
            return expectedCompletionItem;
        }
 
        private static ClassifiedTextRun[] CreateClassifiedTextRunForClass(string className)
            =>
            [
                new ClassifiedTextRun("whitespace", string.Empty),
                new ClassifiedTextRun("keyword", "class"),
                new ClassifiedTextRun("whitespace", " "),
                new ClassifiedTextRun("class name", className),
                new ClassifiedTextRun("whitespace", string.Empty),
            ];
 
        private static async Task<T> GetCompletionItemToResolveAsync<T>(
            TestLspServer testLspServer,
            string label) where T : LSP.CompletionItem
        {
            var completionParams = CreateCompletionParams(
                testLspServer.GetLocations("caret").Single(), LSP.VSInternalCompletionInvokeKind.Explicit, "\0", LSP.CompletionTriggerKind.Invoked);
 
            var completionList = await RunGetCompletionsAsync(testLspServer, completionParams);
 
            if (testLspServer.ClientCapabilities.HasCompletionListDataCapability())
            {
                var vsCompletionList = Assert.IsAssignableFrom<VSInternalCompletionList>(completionList);
                Assert.NotNull(vsCompletionList.Data);
            }
 
            var clientCompletionItem = (T)completionList.Items.FirstOrDefault(item => item.Label == label);
            return clientCompletionItem;
        }
 
        private static async Task<LSP.CompletionList> RunGetCompletionsAsync(
            TestLspServer testLspServer,
            LSP.CompletionParams completionParams)
        {
            var completionList = await testLspServer.ExecuteRequestAsync<LSP.CompletionParams, LSP.CompletionList>(LSP.Methods.TextDocumentCompletionName,
                completionParams, CancellationToken.None);
 
            // Emulate client behavior of promoting "Data" completion list properties onto completion items.
            if (testLspServer.ClientCapabilities.HasCompletionListDataCapability() &&
                completionList is VSInternalCompletionList vsCompletionList &&
                vsCompletionList.Data != null)
            {
                foreach (var completionItem in completionList.Items)
                {
                    Assert.Null(completionItem.Data);
                    completionItem.Data = vsCompletionList.Data;
                }
            }
 
            return completionList;
        }
 
        private class TestCaretOutOfScopeCompletionService : CompletionService
        {
            public TestCaretOutOfScopeCompletionService(SolutionServices services) : base(services, AsynchronousOperationListenerProvider.NullProvider)
            {
            }
 
            public override string Language => LanguageNames.CSharp;
 
            internal override Task<CodeAnalysis.Completion.CompletionList> GetCompletionsAsync(Document document,
                int caretPosition,
                CodeAnalysis.Completion.CompletionOptions options,
                OptionSet passThroughOptions,
                CompletionTrigger trigger = default,
                ImmutableHashSet<string> roles = null,
                CancellationToken cancellationToken = default) => Task.FromResult(CodeAnalysis.Completion.CompletionList.Empty);
 
            public override Task<CompletionChange> GetChangeAsync(
                Document document,
                CodeAnalysis.Completion.CompletionItem item,
                char? commitCharacter = null,
                CancellationToken cancellationToken = default)
            {
                var textChange = new TextChange(span: new TextSpan(start: 77, length: 9), newText: @"public override void M()
    {
        throw new System.NotImplementedException();
    }");
 
                return Task.FromResult(CompletionChange.Create(textChange, newPosition: 0));
            }
 
            internal override bool ShouldTriggerCompletion(Project project, LanguageServices languageServices, SourceText text, int caretPosition, CompletionTrigger trigger, CodeAnalysis.Completion.CompletionOptions options, OptionSet passthroughOptions, ImmutableHashSet<string> roles = null)
                => false;
 
            internal override CompletionRules GetRules(CodeAnalysis.Completion.CompletionOptions options)
                => CompletionRules.Default;
 
            internal override Task<CompletionDescription> GetDescriptionAsync(Document document, CodeAnalysis.Completion.CompletionItem item, CodeAnalysis.Completion.CompletionOptions options, SymbolDescriptionOptions displayOptions, CancellationToken cancellationToken = default)
                => Task.FromResult(CompletionDescription.Empty);
        }
    }
}