File: Completion\CommonCompletionProvider.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.Immutable;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Snippets;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.Completion;
 
internal abstract class CommonCompletionProvider : CompletionProvider
{
    private static readonly CompletionItemRules s_suggestionItemRules = CompletionItemRules.Create(enterKeyRule: EnterKeyRule.Never);
 
    /// <summary>
    /// Language used to retrieve <see cref="CompletionOptions"/> from <see cref="OptionSet"/>.
    /// Null for language agnostic values.
    /// </summary>
    internal abstract string Language { get; }
 
    /// <summary>
    /// For backwards API compat only, should not be called.
    /// </summary>
    public sealed override bool ShouldTriggerCompletion(SourceText text, int caretPosition, CompletionTrigger trigger, OptionSet options)
    {
        Debug.Fail("For backwards API compat only, should not be called");
 
        // Publicly available options do not affect this API.
        return ShouldTriggerCompletionImpl(text, caretPosition, trigger, CompletionOptions.Default);
    }
 
    internal override bool ShouldTriggerCompletion(LanguageServices languageServices, SourceText text, int caretPosition, CompletionTrigger trigger, CompletionOptions options, OptionSet passThroughOptions)
        => ShouldTriggerCompletionImpl(text, caretPosition, trigger, options);
 
    private bool ShouldTriggerCompletionImpl(SourceText text, int caretPosition, CompletionTrigger trigger, in CompletionOptions options)
        => trigger.Kind == CompletionTriggerKind.Insertion &&
           caretPosition > 0 &&
           IsInsertionTrigger(text, insertedCharacterPosition: caretPosition - 1, options);
 
    public virtual bool IsInsertionTrigger(SourceText text, int insertedCharacterPosition, CompletionOptions options)
        => false;
 
    /// <summary>
    /// For backwards API compat only, should not be called.
    /// </summary>
    public sealed override Task<CompletionDescription?> GetDescriptionAsync(Document document, CompletionItem item, CancellationToken cancellationToken)
    {
        Debug.Fail("For backwards API compat only, should not be called");
 
        // Publicly available options do not affect this API.
        return GetDescriptionAsync(document, item, CompletionOptions.Default, SymbolDescriptionOptions.Default, cancellationToken);
    }
 
    internal override async Task<CompletionDescription?> GetDescriptionAsync(Document document, CompletionItem item, CompletionOptions options, SymbolDescriptionOptions displayOptions, CancellationToken cancellationToken)
    {
        // Get the actual description provided by whatever subclass we are.
        // Then, if we would commit text that could be expanded as a snippet, 
        // put that information in the description so that the user knows.
        var description = await GetDescriptionWorkerAsync(document, item, options, displayOptions, cancellationToken).ConfigureAwait(false);
        var parts = await TryAddSnippetInvocationPartAsync(document, item, description.TaggedParts, cancellationToken).ConfigureAwait(false);
 
        return description.WithTaggedParts(parts);
    }
 
    private async Task<ImmutableArray<TaggedText>> TryAddSnippetInvocationPartAsync(
        Document document, CompletionItem item,
        ImmutableArray<TaggedText> parts, CancellationToken cancellationToken)
    {
        var snippetService = document.Project.Services.GetService<ISnippetInfoService>();
        if (snippetService != null)
        {
            var change = await GetTextChangeAsync(document, item, ch: '\t', cancellationToken: cancellationToken).ConfigureAwait(false) ??
                new TextChange(item.Span, item.DisplayText);
            var insertionText = change.NewText;
 
            if (snippetService != null && snippetService.SnippetShortcutExists_NonBlocking(insertionText))
            {
                var note = string.Format(FeaturesResources.Note_colon_Tab_twice_to_insert_the_0_snippet, insertionText);
 
                if (parts.Any())
                {
                    parts = parts.Add(new TaggedText(TextTags.LineBreak, Environment.NewLine));
                }
 
                parts = parts.Add(new TaggedText(TextTags.Text, note));
            }
        }
 
        return parts;
    }
 
    internal virtual Task<CompletionDescription> GetDescriptionWorkerAsync(
        Document document, CompletionItem item, CompletionOptions options, SymbolDescriptionOptions displayOptions, CancellationToken cancellationToken)
    {
        return CommonCompletionItem.HasDescription(item)
            ? Task.FromResult(CommonCompletionItem.GetDescription(item))
            : Task.FromResult(CompletionDescription.Empty);
    }
 
    public override async Task<CompletionChange> GetChangeAsync(Document document, CompletionItem item, char? commitKey = null, CancellationToken cancellationToken = default)
    {
        var change = (await GetTextChangeAsync(document, item, commitKey, cancellationToken).ConfigureAwait(false))
            ?? new TextChange(item.Span, item.DisplayText);
        return CompletionChange.Create(change);
    }
 
    public virtual Task<TextChange?> GetTextChangeAsync(Document document, CompletionItem selectedItem, char? ch, CancellationToken cancellationToken)
        => GetTextChangeAsync(selectedItem, ch, cancellationToken);
 
    protected virtual Task<TextChange?> GetTextChangeAsync(CompletionItem selectedItem, char? ch, CancellationToken cancellationToken)
        => SpecializedTasks.Default<TextChange?>();
 
    protected static CompletionItem CreateSuggestionModeItem(string? displayText, string? description)
    {
        return CommonCompletionItem.Create(
            displayText: displayText ?? string.Empty,
            displayTextSuffix: "",
            description: description == null ? default : description.ToSymbolDisplayParts(),
            rules: s_suggestionItemRules);
    }
}