File: Completion\CompletionServiceTests.cs
Web Access
Project: src\src\EditorFeatures\Test\Microsoft.CodeAnalysis.EditorFeatures.UnitTests.csproj (Microsoft.CodeAnalysis.EditorFeatures.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;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Completion;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Shared.TestHooks;
using Microsoft.CodeAnalysis.Test.Utilities;
using Microsoft.CodeAnalysis.Text;
using Xunit;
 
namespace Microsoft.CodeAnalysis.Editor.UnitTests.Completion;
 
[UseExportProvider]
public class CompletionServiceTests
{
    [Fact]
    public async Task TestNuGetCompletionProvider()
    {
        var code = @"
using System.Diagnostics;
class Test {
    void Method() {
        Debug.Assert(true, ""$$"");
    }
}
";
 
        using var workspace = TestWorkspace.CreateCSharp(code, openDocuments: true);
 
        var nugetCompletionProvider = new DebugAssertTestCompletionProvider();
        var reference = new MockAnalyzerReference(nugetCompletionProvider);
        var project = workspace.CurrentSolution.Projects.Single().AddAnalyzerReference(reference);
        var completionService = project.Services.GetRequiredService<CompletionService>();
 
        var document = project.Documents.Single();
        var caretPosition = workspace.DocumentWithCursor.CursorPosition ?? throw new InvalidOperationException();
        var completions = await completionService.GetCompletionsAsync(document, caretPosition, CompletionOptions.Default, OptionSet.Empty);
 
        // NuGet providers are not included until it's loaded and cached, this is to avoid potential delays, especially on UI thread.
        Assert.Empty(completions.ItemsList);
 
        // NuGet analyzers for the project will be loaded when this returns 
        var waiter = workspace.ExportProvider.GetExportedValue<AsynchronousOperationListenerProvider>().GetWaiter(FeatureAttribute.CompletionSet);
        await waiter.ExpeditedWaitAsync();
 
        completions = await completionService.GetCompletionsAsync(document, caretPosition, CompletionOptions.Default, OptionSet.Empty);
 
        Assert.NotEmpty(completions.ItemsList);
 
        var item = Assert.Single(completions.ItemsList.Where(item => item.ProviderName == typeof(DebugAssertTestCompletionProvider).FullName));
        Assert.Equal(nameof(DebugAssertTestCompletionProvider), item.DisplayText);
 
        var expectedDescriptionText = nameof(DebugAssertTestCompletionProvider);
        var actualDescriptionText = (await completionService.GetDescriptionAsync(document, item, CompletionOptions.Default, SymbolDescriptionOptions.Default).ConfigureAwait(false))!.Text;
        Assert.Equal(expectedDescriptionText, actualDescriptionText);
 
        var expectedChange = new TextChange(item.Span, nameof(DebugAssertTestCompletionProvider));
        var actualChange = (await completionService.GetChangeAsync(document, item).ConfigureAwait(false)).TextChange;
        Assert.Equal(expectedChange, actualChange);
    }
 
    private class MockAnalyzerReference : AnalyzerReference, ICompletionProviderFactory
    {
        private readonly CompletionProvider _completionProvider;
 
        public MockAnalyzerReference(CompletionProvider completionProvider)
        {
            _completionProvider = completionProvider;
        }
 
        public override string FullPath => "";
        public override object Id => nameof(MockAnalyzerReference);
 
        public override ImmutableArray<DiagnosticAnalyzer> GetAnalyzers(string language)
            => ImmutableArray<DiagnosticAnalyzer>.Empty;
 
        public override ImmutableArray<DiagnosticAnalyzer> GetAnalyzersForAllLanguages()
            => ImmutableArray<DiagnosticAnalyzer>.Empty;
 
        public ImmutableArray<CompletionProvider> GetCompletionProviders()
            => ImmutableArray.Create(_completionProvider);
    }
 
    private sealed class DebugAssertTestCompletionProvider : CompletionProvider
    {
        public DebugAssertTestCompletionProvider()
        {
        }
 
        public override bool ShouldTriggerCompletion(SourceText text, int caretPosition, CompletionTrigger trigger, OptionSet options)
        {
            return trigger.Kind switch
            {
                CompletionTriggerKind.Invoke => true,
                CompletionTriggerKind.InvokeAndCommitIfUnique => true,
                CompletionTriggerKind.Insertion => trigger.Character == '"',
                _ => false,
            };
        }
 
        public override async Task ProvideCompletionsAsync(CompletionContext context)
        {
            var completionItem = CompletionItem.Create(displayText: nameof(DebugAssertTestCompletionProvider), displayTextSuffix: "", rules: CompletionItemRules.Default);
            context.AddItem(completionItem);
            context.CompletionListSpan = await GetTextChangeSpanAsync(context.Document, context.CompletionListSpan, context.CancellationToken).ConfigureAwait(false);
        }
 
        public override Task<CompletionDescription> GetDescriptionAsync(Document document, CompletionItem item, CancellationToken cancellationToken)
        {
            return Task.FromResult(CompletionDescription.FromText(nameof(DebugAssertTestCompletionProvider)));
        }
 
        public override Task<CompletionChange> GetChangeAsync(Document document, CompletionItem item, char? commitKey, CancellationToken cancellationToken)
        {
            return Task.FromResult(CompletionChange.Create(new TextChange(item.Span, nameof(DebugAssertTestCompletionProvider))));
        }
 
        private static async Task<TextSpan> GetTextChangeSpanAsync(Document document, TextSpan startSpan, CancellationToken cancellationToken)
        {
            var result = startSpan;
            var syntaxFacts = document.GetLanguageService<ISyntaxFactsService>();
            var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
            var token = root.FindToken(result.Start);
            if (syntaxFacts.IsStringLiteral(token) || syntaxFacts.IsVerbatimStringLiteral(token))
            {
                var text = root.GetText();
 
                // Expand selection in both directions until a double quote or any line break character is reached
                static bool IsWordCharacter(char ch) => !(ch == '"' || TextUtilities.IsAnyLineBreakCharacter(ch));
 
                result = CommonCompletionUtilities.GetWordSpan(
                    text, startSpan.Start, IsWordCharacter, IsWordCharacter, alwaysExtendEndSpan: true);
            }
 
            return result;
        }
    }
}