File: Completion\CompletionTriggerAndCommitCharacters.cs
Web Access
Project: src\src\Razor\src\Razor\src\Microsoft.CodeAnalysis.Razor.Workspaces\Microsoft.CodeAnalysis.Razor.Workspaces.csproj (Microsoft.CodeAnalysis.Razor.Workspaces)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Collections.Frozen;
using System.Collections.Generic;
using System.Linq;
using Microsoft.CodeAnalysis.Razor.Protocol;
 
namespace Microsoft.CodeAnalysis.Razor.Completion;
 
internal class CompletionTriggerAndCommitCharacters(IClientCapabilitiesService clientCapabilitiesService)
{
    /// <summary>
    ///  Trigger character that can trigger both Razor and Delegation completion
    /// </summary>
    private const char TransitionCharacter = '@';
 
    private static readonly string[] s_commitCharacters = [" ", ">", ";", "="];
 
    private readonly IClientCapabilitiesService _clientCapabilitiesService = clientCapabilitiesService;
 
    private static readonly FrozenSet<char> _csharpTriggerCharacters = [' ', '(', '=', '#', '.', '<', '[', '{', '"', '/', ':', '~'];
    private static readonly FrozenSet<char> _vsHtmlTriggerCharacters = [TransitionCharacter, ':', '#', '.', '!', '*', ',', '(', '[', '-', '<', '&', '\\', '/', '\'', '"', '=', ':', ' ', '`'];
    private static readonly FrozenSet<char> _vsCodeHtmlTriggerCharacters = [TransitionCharacter, '#', '.', '!', ',', '<'];
    private static readonly FrozenSet<char> _razorTriggerCharacters = [TransitionCharacter, '<', ':', ' '];
 
    private FrozenSet<char> HtmlTriggerCharacters => _clientCapabilitiesService.ClientCapabilities.SupportsVisualStudioExtensions
        ? _vsHtmlTriggerCharacters
        : _vsCodeHtmlTriggerCharacters;
 
    private FrozenSet<char> DelegationTriggerCharacters => field ??= ComputeDelegationTriggerCharacters();
 
    public string[] AllTriggerCharacters => field ??= ComputeAllTriggerCharacters();
 
    /// <summary>
    /// This is the intersection of C# and HTML commit characters.
    /// </summary>
    /// <remarks>
    /// <para>
    /// We need to specify it so that platform can correctly calculate ApplicableToSpan
    /// This is needed to fix https://github.com/dotnet/razor/issues/10787 in particular
    /// </para>
    /// <para>
    /// However we shouldn't specify commit characters for VSCode, as it doesn't appear to need
    /// them and they interfere with normal item commit. e.g. see https://github.com/dotnet/vscode-csharp/issues/7678
    /// </para>
    /// </remarks>
    public string[] AllCommitCharacters => _clientCapabilitiesService.ClientCapabilities.SupportsVisualStudioExtensions ? s_commitCharacters : [];
 
    private FrozenSet<char> ComputeDelegationTriggerCharacters()
    {
        // Delegation trigger characters (include '@' + C# and HTML trigger characters)
        var delegationTriggerCharacters = new HashSet<char> { TransitionCharacter };
        delegationTriggerCharacters.UnionWith(_csharpTriggerCharacters);
        delegationTriggerCharacters.UnionWith(HtmlTriggerCharacters);
 
        return delegationTriggerCharacters.ToFrozenSet();
    }
 
    private string[] ComputeAllTriggerCharacters()
    {
        // All trigger characters (include Razor + Delegation trigger characters)
        var allTriggerCharacters = new HashSet<char>();
        allTriggerCharacters.UnionWith(_razorTriggerCharacters);
        allTriggerCharacters.UnionWith(DelegationTriggerCharacters);
 
        return allTriggerCharacters.Select(static c => c.ToString()).ToArray();
    }
 
    public bool IsValidCSharpTrigger(CompletionContext completionContext)
        => IsValidTrigger(completionContext, _csharpTriggerCharacters);
 
    public bool IsValidDelegationTrigger(CompletionContext completionContext)
        => IsValidTrigger(completionContext, DelegationTriggerCharacters);
 
    public bool IsValidHtmlTrigger(CompletionContext completionContext)
        => IsValidTrigger(completionContext, HtmlTriggerCharacters);
 
    public bool IsValidRazorTrigger(CompletionContext completionContext)
        => IsValidTrigger(completionContext, _razorTriggerCharacters);
 
    private static bool IsValidTrigger(CompletionContext completionContext, FrozenSet<char> triggerCharacters)
        => completionContext.TriggerKind != CompletionTriggerKind.TriggerCharacter ||
           completionContext.TriggerCharacter is not [var c] ||
           triggerCharacters.Contains(c);
 
    public bool IsCSharpTriggerCharacter(string ch)
        => ch is [var c] && _csharpTriggerCharacters.Contains(c);
 
    public bool IsDelegationTriggerCharacter(string ch)
        => ch is [var c] && DelegationTriggerCharacters.Contains(c);
 
    public bool IsHtmlTriggerCharacter(string ch)
        => ch is [var c] && HtmlTriggerCharacters.Contains(c);
 
    public bool IsRazorTriggerCharacter(string ch)
        => ch is [var c] && _razorTriggerCharacters.Contains(c);
 
    public bool IsTransitionCharacter(string ch)
        => ch is [TransitionCharacter];
}