File: Completion\CompletionService.cs
Web Access
Project: src\src\Features\Core\Portable\Microsoft.CodeAnalysis.Features.csproj (Microsoft.CodeAnalysis.Features)
// 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.
 
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Collections;
using Microsoft.CodeAnalysis.Extensions;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Shared.TestHooks;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.Completion;
 
/// <summary>
/// A per language service for constructing context dependent list of completions that 
/// can be presented to a user during typing in an editor. It aggregates completions from
/// one or more <see cref="CompletionProvider"/>s.
/// </summary>
public abstract partial class CompletionService : ILanguageService
{
    private readonly SolutionServices _services;
    private readonly ProviderManager _providerManager;
 
    /// <summary>
    /// Test-only switch.
    /// </summary>
    private bool _suppressPartialSemantics;
 
    // Prevent inheritance outside of Roslyn.
    internal CompletionService(SolutionServices services, IAsynchronousOperationListenerProvider listenerProvider)
    {
        _services = services;
        _providerManager = new(this, listenerProvider);
    }
 
    /// <summary>
    /// Gets the service corresponding to the specified document.
    /// </summary>
    public static CompletionService? GetService(Document? document)
        => document?.GetLanguageService<CompletionService>();
 
    /// <summary>
    /// Returns the providers always available to the service.
    /// This does not included providers imported via MEF composition.
    /// </summary>
    [Obsolete("Built-in providers will be ignored in a future release, please make them MEF exports instead.")]
    protected virtual ImmutableArray<CompletionProvider> GetBuiltInProviders()
        => [];
 
    /// <summary>
    /// The language from <see cref="LanguageNames"/> this service corresponds to.
    /// </summary>
    public abstract string Language { get; }
 
    /// <summary>
    /// Gets the current presentation and behavior rules.
    /// </summary>
    /// <remarks>
    /// Backward compatibility only.
    /// </remarks>
    public CompletionRules GetRules()
    {
        Debug.Fail("For backwards API compat only, should not be called");
 
        // Publicly available options do not affect this API.
        return GetRules(CompletionOptions.Default);
    }
 
    internal abstract CompletionRules GetRules(CompletionOptions options);
 
    /// <summary>
    /// Returns true if the character recently inserted or deleted in the text should trigger completion.
    /// </summary>
    /// <param name="text">The document text to trigger completion within </param>
    /// <param name="caretPosition">The position of the caret after the triggering action.</param>
    /// <param name="trigger">The potential triggering action.</param>
    /// <param name="roles">Optional set of roles associated with the editor state.</param>
    /// <param name="options">Optional options that override the default options.</param>
    /// <remarks>
    /// This API uses SourceText instead of Document so implementations can only be based on text, not syntax or semantics.
    /// </remarks>
    public bool ShouldTriggerCompletion(
        SourceText text,
        int caretPosition,
        CompletionTrigger trigger,
        ImmutableHashSet<string>? roles = null,
        OptionSet? options = null)
    {
        var document = text.GetOpenDocumentInCurrentContextWithChanges();
        var languageServices = document?.Project.Services ?? _services.GetLanguageServices(Language);
 
        // Publicly available options do not affect this API. Force complete results from this public API since
        // external consumers do not have access to Roslyn's waiters.
        var completionOptions = CompletionOptions.Default with { ForceExpandedCompletionIndexCreation = true };
        var passThroughOptions = options ?? document?.Project.Solution.Options ?? OptionSet.Empty;
 
        return ShouldTriggerCompletion(document?.Project, languageServices, text, caretPosition, trigger, completionOptions, passThroughOptions, roles);
    }
 
    internal virtual bool SupportsTriggerOnDeletion(CompletionOptions options)
        => options.TriggerOnDeletion == true;
 
    /// <summary>
    /// Returns true if the character recently inserted or deleted in the text should trigger completion.
    /// </summary>
    /// <param name="project">The project containing the document and text</param>
    /// <param name="languageServices">Language services</param>
    /// <param name="text">The document text to trigger completion within </param>
    /// <param name="caretPosition">The position of the caret after the triggering action.</param>
    /// <param name="trigger">The potential triggering action.</param>
    /// <param name="options">Options.</param>
    /// <param name="passThroughOptions">Options originating either from external caller of the <see cref="CompletionService"/> or set externally to <see cref="Solution.Options"/>.</param>
    /// <param name="roles">Optional set of roles associated with the editor state.</param>
    /// <remarks>
    /// We pass the project here to retrieve information about the <see cref="Project.AnalyzerReferences"/>,
    /// <see cref="WorkspaceKind"/> and <see cref="Project.Language"/> which are fast operations.
    /// It should not be used for syntactic or semantic operations.
    /// </remarks>
    internal virtual bool ShouldTriggerCompletion(
        Project? project,
        LanguageServices languageServices,
        SourceText text,
        int caretPosition,
        CompletionTrigger trigger,
        CompletionOptions options,
        OptionSet passThroughOptions,
        ImmutableHashSet<string>? roles = null)
    {
        // The trigger kind guarantees that user wants a completion.
        if (trigger.Kind is CompletionTriggerKind.Invoke or CompletionTriggerKind.InvokeAndCommitIfUnique)
            return true;
 
        if (!options.TriggerOnTyping)
            return false;
 
        // Enter does not trigger completion.
        if (trigger.Kind == CompletionTriggerKind.Insertion && trigger.Character == '\n')
        {
            return false;
        }
 
        if (trigger.Kind == CompletionTriggerKind.Deletion && SupportsTriggerOnDeletion(options))
        {
            return char.IsLetterOrDigit(trigger.Character) || trigger.Character == '.';
        }
 
        var extensionManager = languageServices.SolutionServices.GetRequiredService<IExtensionManager>();
 
        var providers = _providerManager.GetFilteredProviders(project, roles, trigger, options);
        return providers.Any(p =>
            extensionManager.PerformFunction(p,
                () => p.ShouldTriggerCompletion(languageServices, text, caretPosition, trigger, options, passThroughOptions),
                defaultValue: false));
    }
 
    /// <summary>
    /// Gets the span of the syntax element at the caret position.
    /// This is the most common value used for <see cref="CompletionItem.Span"/>.
    /// </summary>
    /// <param name="text">The document text that completion is occurring within.</param>
    /// <param name="caretPosition">The position of the caret within the text.</param>
    [Obsolete("Not used anymore. CompletionService.GetDefaultCompletionListSpan is used instead.", error: true)]
    public virtual TextSpan GetDefaultItemSpan(SourceText text, int caretPosition)
        => GetDefaultCompletionListSpan(text, caretPosition);
 
    public virtual TextSpan GetDefaultCompletionListSpan(SourceText text, int caretPosition)
    {
        return CommonCompletionUtilities.GetWordSpan(
            text, caretPosition, char.IsLetter, char.IsLetterOrDigit);
    }
 
    /// <summary>
    /// Gets the description of the item.
    /// </summary>
    /// <param name="document">This will be the  original document that
    /// <paramref name="item"/> was created against.</param>
    /// <param name="item">The item to get the description for.</param>
    /// <param name="cancellationToken"></param>
    public Task<CompletionDescription?> GetDescriptionAsync(
        Document document,
        CompletionItem item,
        CancellationToken cancellationToken = default)
    {
        // Publicly available options do not affect this API.
        return GetDescriptionAsync(document, item, CompletionOptions.Default, SymbolDescriptionOptions.Default, cancellationToken);
    }
 
    /// <summary>
    /// Gets the description of the item.
    /// </summary>
    /// <param name="document">This will be the  original document that
    /// <paramref name="item"/> was created against.</param>
    /// <param name="item">The item to get the description for.</param>
    /// <param name="options">Completion options</param>
    /// <param name="displayOptions">Display options</param>
    /// <param name="cancellationToken"></param>
    internal virtual async Task<CompletionDescription?> GetDescriptionAsync(Document document, CompletionItem item, CompletionOptions options, SymbolDescriptionOptions displayOptions, CancellationToken cancellationToken = default)
    {
        var provider = GetProvider(item, document.Project);
        if (provider is null)
            return CompletionDescription.Empty;
 
        var extensionManager = document.Project.Solution.Services.GetRequiredService<IExtensionManager>();
 
        // We don't need SemanticModel here, just want to make sure it won't get GC'd before CompletionProviders are able to get it.
        document = GetDocumentWithFrozenPartialSemantics(document, cancellationToken);
        var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false);
        var description = await extensionManager.PerformFunctionAsync(
            provider,
            cancellationToken => provider.GetDescriptionAsync(document, item, options, displayOptions, cancellationToken),
            defaultValue: null,
            cancellationToken).ConfigureAwait(false);
        GC.KeepAlive(semanticModel);
        return description;
    }
 
    /// <summary>
    /// Gets the change to be applied when the item is committed.
    /// </summary>
    /// <param name="document">The document that completion is occurring within.</param>
    /// <param name="item">The item to get the change for.</param>
    /// <param name="commitCharacter">The typed character that caused the item to be committed. 
    /// This character may be used as part of the change. 
    /// This value is null when the commit was caused by the [TAB] or [ENTER] keys.</param>
    /// <param name="cancellationToken"></param>
    public virtual async Task<CompletionChange> GetChangeAsync(
        Document document,
        CompletionItem item,
        char? commitCharacter = null,
        CancellationToken cancellationToken = default)
    {
        var provider = GetProvider(item, document.Project);
        if (provider != null)
        {
            var extensionManager = document.Project.Solution.Services.GetRequiredService<IExtensionManager>();
 
            // We don't need SemanticModel here, just want to make sure it won't get GC'd before CompletionProviders are able to get it.
            document = GetDocumentWithFrozenPartialSemantics(document, cancellationToken);
            var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false);
 
            var change = await extensionManager.PerformFunctionAsync(
                provider,
                cancellationToken => provider.GetChangeAsync(document, item, commitCharacter, cancellationToken),
                defaultValue: null!,
                cancellationToken).ConfigureAwait(false);
            if (change == null)
                return CompletionChange.Create(new TextChange(new TextSpan(), ""));
 
            GC.KeepAlive(semanticModel);
            Debug.Assert(item.Span == change.TextChange.Span || item.IsComplexTextEdit);
            return change;
        }
        else
        {
            return CompletionChange.Create(new TextChange(item.Span, item.DisplayText));
        }
    }
 
    // The FilterItems method might need to handle a large list of items when import completion is enabled and filter text is
    // very short, i.e. <= 1. Therefore, use pooled list to avoid repeated (potentially LOH) allocations.
    private static readonly ObjectPool<List<MatchResult>> s_listOfMatchResultPool = new(factory: () => []);
 
    /// <summary>
    /// Given a list of completion items that match the current code typed by the user,
    /// returns the item that is considered the best match, and whether or not that
    /// item should be selected or not.
    /// 
    /// itemToFilterText provides the values that each individual completion item should
    /// be filtered against.
    /// </summary>
    public virtual ImmutableArray<CompletionItem> FilterItems(
        Document document,
        ImmutableArray<CompletionItem> items,
        string filterText)
    {
        using var helper = new PatternMatchHelper(filterText);
        var filterDataList = new SegmentedList<MatchResult>(
            items.Select(item => helper.GetMatchResult(item, includeMatchSpans: false, CultureInfo.CurrentCulture)));
 
        var builder = s_listOfMatchResultPool.Allocate();
        try
        {
            FilterItems(CompletionHelper.GetHelper(document), filterDataList, filterText, builder);
            return builder.SelectAsArray(result => result.CompletionItem);
        }
        finally
        {
            // Don't call ClearAndFree, which resets the capacity to a default value.
            builder.Clear();
            s_listOfMatchResultPool.Free(builder);
        }
    }
 
    internal virtual void FilterItems(
       Document document,
       IReadOnlyList<MatchResult> matchResults,
       string filterText,
       IList<MatchResult> builder)
    {
#pragma warning disable RS0030 // Do not used banned APIs
        // Default implementation just drops the pattern matches and builder, and calls the public overload of FilterItems instead for compatibility.
        var filteredItems = FilterItems(document, matchResults.SelectAsArray(item => item.CompletionItem), filterText);
#pragma warning restore RS0030 // Do not used banned APIs
 
        using var completionPatternMatchers = new PatternMatchHelper(filterText);
        builder.AddRange(filteredItems.Select(item => completionPatternMatchers.GetMatchResult(item, includeMatchSpans: false, CultureInfo.CurrentCulture)));
    }
 
    /// <summary>
    /// Determine among the provided items the best match w.r.t. the given filter text, 
    /// those returned would be considered equally good candidates for selection by controller.
    /// </summary>
    internal static void FilterItems(
        CompletionHelper completionHelper,
        IReadOnlyList<MatchResult> matchResults,
        string filterText,
        IList<MatchResult> builder)
    {
        // It's very common for people to type expecting completion to fix up their casing,
        // so if no uppercase characters were typed so far, we'd loosen our standard on comparing items
        // in terms of case-sensitivity and take into consideration the MatchPriority in certain scenarios.
        // i.e. when everything else is equal, if item1 is a better case-sensitive match but has
        // MatchPriority.Deprioritize, and item2 is not MatchPriority.Deprioritize, then we consider
        // item2 a better match.
        var filterTextHasNoUpperCase = !filterText.Any(char.IsUpper);
 
        foreach (var matchResult in matchResults)
        {
            if (!matchResult.ShouldBeConsideredMatchingFilterText)
                continue;
 
            if (builder.Count == 0)
            {
                // We've found no good items yet.  So this is the best item currently.
                builder.Add(matchResult);
                continue;
            }
 
            var comparison = completionHelper.CompareMatchResults(matchResult, builder[0], filterTextHasNoUpperCase);
 
            if (comparison == 0)
            {
                // This item is as good as the items we've been collecting.  We'll return it and let the controller
                // decide what to do.  (For example, it will pick the one that has the best MRU index).
                builder.Add(matchResult);
            }
            else if (comparison < 0)
            {
                // This item is strictly better than the best items we've found so far.
                builder.Clear();
                builder.Add(matchResult);
            }
        }
    }
 
    internal static bool IsAllPunctuation(string filterText)
    {
        foreach (var ch in filterText)
        {
            if (!char.IsPunctuation(ch))
            {
                return false;
            }
        }
 
        return true;
    }
 
    /// <summary>
    /// Don't call. Used for pre-populating MEF providers only.
    /// </summary>
    internal void LoadImportedProviders()
        => _providerManager.LoadProviders();
 
    /// <summary>
    /// Don't call. Used for pre-load project providers only.
    /// </summary>
    internal void TriggerLoadProjectProviders(Project project, CompletionOptions options)
            => _providerManager.GetCachedProjectCompletionProvidersOrQueueLoadInBackground(project, options);
 
    internal CompletionProvider? GetProvider(CompletionItem item, Project? project)
        => _providerManager.GetProvider(item, project);
 
    internal TestAccessor GetTestAccessor()
        => new(this);
 
    internal readonly struct TestAccessor(CompletionService completionServiceWithProviders)
    {
        private readonly CompletionService _completionServiceWithProviders = completionServiceWithProviders;
 
        public ImmutableArray<CompletionProvider> GetImportedAndBuiltInProviders(ImmutableHashSet<string> roles)
            => _completionServiceWithProviders._providerManager.GetTestAccessor().GetImportedAndBuiltInProviders(roles);
 
        public ImmutableArray<CompletionProvider> GetProjectProviders(Project project)
            => _completionServiceWithProviders._providerManager.GetTestAccessor().GetProjectProviders(project);
 
        public async Task<CompletionContext> GetContextAsync(
            CompletionProvider provider,
            Document document,
            int position,
            CompletionTrigger triggerInfo,
            CompletionOptions options,
            CancellationToken cancellationToken)
        {
            var text = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false);
            var defaultItemSpan = _completionServiceWithProviders.GetDefaultCompletionListSpan(text, position);
 
            return await CompletionService.GetContextAsync(
                provider,
                document,
                position,
                triggerInfo,
                options,
                defaultItemSpan,
                sharedContext: null,
                cancellationToken).ConfigureAwait(false);
        }
 
        public void SuppressPartialSemantics()
            => _completionServiceWithProviders._suppressPartialSemantics = true;
    }
}