File: Completion\DirectiveAttributeTransitionCompletionItemProvider.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.
 
#if !NET
using System;
#endif
using System.Collections.Immutable;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.Language.Syntax;
using Microsoft.CodeAnalysis.Razor.Protocol;
using Microsoft.CodeAnalysis.Text;
 
namespace Microsoft.CodeAnalysis.Razor.Completion;
 
internal class DirectiveAttributeTransitionCompletionItemProvider(IClientCapabilitiesService clientCapabilitiesService) : DirectiveAttributeCompletionItemProviderBase
{
    private const string DisplayText = "@...";
    private static readonly DirectiveCompletionDescription s_descriptionInfo = new(SR.Blazor_directive_attributes);
 
    private RazorCompletionItem? _transitionCompletionItem;
 
    public RazorCompletionItem TransitionCompletionItem
        => _transitionCompletionItem ??= RazorCompletionItem.CreateDirective(
            displayText: DisplayText,
            insertText: "@",
            sortText: null,
            descriptionInfo: s_descriptionInfo,
 
            // We specify these three commit characters to work around a Visual Studio interaction where
            // completion items that get "soft selected" will cause completion to re-trigger if a user
            // types one of the soft-selected completion item's commit characters.
            // In practice this happens in the `<button |` scenario where the "space" results in completions
            // where this directive attribute transition character ("@...") gets provided and then typing
            // `@` should re-trigger OR typing `/` should re-trigger.
            // However, in VS Code explicit commit characters like these cause issues, e.g. "@..." gets committed when trying to type "/" in a
            // self-closing tag. So in VS Code we have SupportSoftSelectionInCompletion set to false and we will
            // use empty commit character set in that case.
            commitCharacters: _clientCapabilitiesService.ClientCapabilities.SupportsVisualStudioExtensions ? RazorCommitCharacter.CreateArray(["@", "/", ">"]) : [],
            isSnippet: false);
 
    public static bool IsTransitionCompletionItem(RazorCompletionItem completionItem)
    {
        return completionItem.Kind == RazorCompletionItemKind.Directive && completionItem.DescriptionInfo == s_descriptionInfo && completionItem.DisplayText == DisplayText;
    }
 
    private ImmutableArray<RazorCompletionItem>? _completions;
 
    private ImmutableArray<RazorCompletionItem> Completions => _completions ??= [TransitionCompletionItem];
 
    private readonly IClientCapabilitiesService _clientCapabilitiesService = clientCapabilitiesService;
 
    public override ImmutableArray<RazorCompletionItem> GetCompletionItems(RazorCompletionContext context)
    {
        if (!context.SyntaxTree.Options.FileKind.IsComponent())
        {
            // Directive attributes are only supported in components
            return [];
        }
 
        var owner = context.Owner;
        if (owner is null)
        {
            return [];
        }
 
        var attribute = owner.Parent;
        if (attribute is MarkupMiscAttributeContentSyntax && attribute.ContainsOnlyWhitespace())
        {
            // This represents a tag when there's no attribute content <InputText | />.
            return Completions;
        }
 
        if (!TryGetAttributeInfo(owner, out var prefixLocation, out var attributeName, out var attributeNameLocation, out _, out _))
        {
            return [];
        }
 
        if (attributeNameLocation.IntersectsWith(context.AbsoluteIndex) && attributeName.StartsWith('@'))
        {
            // The transition is already provided for the attribute name
            return [];
        }
 
        if (!IsValidCompletionPoint(context.AbsoluteIndex, prefixLocation, attributeNameLocation))
        {
            // Not operating in the attribute name area
            return [];
        }
 
        // This represents a tag when there's no attribute content <InputText | />.
        return Completions;
    }
 
    // Internal for testing
    internal static bool IsValidCompletionPoint(int absoluteIndex, TextSpan? prefixLocation, TextSpan attributeNameLocation)
    {
        if (absoluteIndex == (prefixLocation?.Start ?? -1))
        {
            // <input| class="test" />
            // Starts of prefix locations belong to the previous SyntaxNode. It could be the end of an attribute value, the tag name, C# etc.
            return false;
        }
 
        if (attributeNameLocation.Start == absoluteIndex)
        {
            // <input |class="test" />
            return false;
        }
 
        if (prefixLocation?.IntersectsWith(absoluteIndex) ?? false)
        {
            // <input   |  class="test" />
            return true;
        }
 
        if (attributeNameLocation.IntersectsWith(absoluteIndex))
        {
            // <input cla|ss="test" />
            return false;
        }
 
        return false;
    }
}