File: Completion\DirectiveAttributeCompletionItemProviderBase.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.Immutable;
using System.Diagnostics.CodeAnalysis;
using Microsoft.AspNetCore.Razor;
using Microsoft.AspNetCore.Razor.Language.Syntax;
using Microsoft.AspNetCore.Razor.PooledObjects;
using Microsoft.CodeAnalysis.Text;
using RazorSyntaxList = Microsoft.AspNetCore.Razor.Language.Syntax.SyntaxList<Microsoft.AspNetCore.Razor.Language.Syntax.SyntaxNode>;
using RazorSyntaxNode = Microsoft.AspNetCore.Razor.Language.Syntax.SyntaxNode;
 
namespace Microsoft.CodeAnalysis.Razor.Completion;
 
internal abstract class DirectiveAttributeCompletionItemProviderBase : IRazorCompletionItemProvider
{
    public abstract ImmutableArray<RazorCompletionItem> GetCompletionItems(RazorCompletionContext context);
 
    // Internal for testing
    internal static bool TryGetAttributeInfo(
        RazorSyntaxNode attributeLeafOwner,
        out TextSpan? prefixLocation,
        [NotNullWhen(true)] out string? attributeName,
        out TextSpan attributeNameLocation,
        out string? parameterName,
        out TextSpan parameterLocation)
    {
        var attribute = attributeLeafOwner.Parent;
 
        // The null check on the `NamePrefix` field is required for cases like:
        // `<svg xml:base=""x| ></svg>` where there's no `NamePrefix` available.
        switch (attribute)
        {
            case MarkupMinimizedAttributeBlockSyntax minimizedMarkupAttribute:
                prefixLocation = minimizedMarkupAttribute.NamePrefix?.Span;
                SplitAttributeNameIntoParts(
                    minimizedMarkupAttribute.Name.GetContent(),
                    minimizedMarkupAttribute.Name.Span,
                    out attributeName,
                    out attributeNameLocation,
                    out parameterName,
                    out parameterLocation);
                return true;
 
            case MarkupAttributeBlockSyntax markupAttribute:
                prefixLocation = markupAttribute.NamePrefix?.Span;
                SplitAttributeNameIntoParts(
                    markupAttribute.Name.GetContent(),
                    markupAttribute.Name.Span,
                    out attributeName,
                    out attributeNameLocation,
                    out parameterName,
                    out parameterLocation);
                return true;
 
            case MarkupMinimizedTagHelperAttributeSyntax minimizedTagHelperAttribute:
                prefixLocation = minimizedTagHelperAttribute.NamePrefix?.Span;
                SplitAttributeNameIntoParts(
                    minimizedTagHelperAttribute.Name.GetContent(),
                    minimizedTagHelperAttribute.Name.Span,
                    out attributeName,
                    out attributeNameLocation,
                    out parameterName,
                    out parameterLocation);
                return true;
 
            case MarkupTagHelperAttributeSyntax tagHelperAttribute:
                prefixLocation = tagHelperAttribute.NamePrefix?.Span;
                SplitAttributeNameIntoParts(
                    tagHelperAttribute.Name.GetContent(),
                    tagHelperAttribute.Name.Span,
                    out attributeName,
                    out attributeNameLocation,
                    out parameterName,
                    out parameterLocation);
                return true;
 
            case MarkupTagHelperDirectiveAttributeSyntax directiveAttribute:
                {
                    var attributeNameNode = directiveAttribute.Name;
                    var directiveAttributeTransition = directiveAttribute.Transition;
                    var nameStart = directiveAttributeTransition?.SpanStart ?? attributeNameNode.SpanStart;
                    var nameEnd = attributeNameNode?.Span.End ?? directiveAttributeTransition.AssumeNotNull().Span.End;
                    prefixLocation = directiveAttribute.NamePrefix?.Span;
                    attributeName = string.Concat(directiveAttributeTransition?.GetContent(), attributeNameNode?.GetContent());
                    attributeNameLocation = new TextSpan(nameStart, nameEnd - nameStart);
                    parameterName = directiveAttribute.ParameterName?.GetContent();
                    parameterLocation = directiveAttribute.ParameterName?.Span ?? default;
                    return true;
                }
 
            case MarkupMinimizedTagHelperDirectiveAttributeSyntax minimizedDirectiveAttribute:
                {
                    var attributeNameNode = minimizedDirectiveAttribute.Name;
                    var directiveAttributeTransition = minimizedDirectiveAttribute.Transition;
                    var nameStart = directiveAttributeTransition?.SpanStart ?? attributeNameNode.SpanStart;
                    var nameEnd = attributeNameNode?.Span.End ?? directiveAttributeTransition.AssumeNotNull().Span.End;
                    prefixLocation = minimizedDirectiveAttribute.NamePrefix?.Span;
                    attributeName = string.Concat(directiveAttributeTransition?.GetContent(), attributeNameNode?.GetContent());
                    attributeNameLocation = new TextSpan(nameStart, nameEnd - nameStart);
                    parameterName = minimizedDirectiveAttribute.ParameterName?.GetContent();
                    parameterLocation = minimizedDirectiveAttribute.ParameterName?.Span ?? default;
                    return true;
                }
        }
 
        prefixLocation = null;
        attributeName = null;
        attributeNameLocation = default;
        parameterName = null;
        parameterLocation = default;
        return false;
    }
 
    // Internal for testing
    internal static bool TryGetElementInfo(
        RazorSyntaxNode element,
        [NotNullWhen(true)] out string? containingTagName,
        out ImmutableArray<string> attributeNames)
    {
        if (element is MarkupStartTagSyntax startTag)
        {
            containingTagName = startTag.Name.Content;
            attributeNames = ExtractAttributeNames(startTag.Attributes);
            return true;
        }
 
        if (element is MarkupTagHelperStartTagSyntax startTagHelper)
        {
            containingTagName = startTagHelper.Name.Content;
            attributeNames = ExtractAttributeNames(startTagHelper.Attributes);
            return true;
        }
 
        containingTagName = null;
        attributeNames = default;
        return false;
    }
 
    private static ImmutableArray<string> ExtractAttributeNames(RazorSyntaxList attributes)
    {
        using var attributeNames = new PooledArrayBuilder<string>(capacity: attributes.Count);
 
        foreach (var attribute in attributes)
        {
            switch (attribute)
            {
                case MarkupTagHelperAttributeSyntax tagHelperAttribute:
                    attributeNames.Add(tagHelperAttribute.Name.GetContent());
                    break;
 
                case MarkupMinimizedTagHelperAttributeSyntax minimizedTagHelperAttribute:
                    attributeNames.Add(minimizedTagHelperAttribute.Name.GetContent());
                    break;
 
                case MarkupAttributeBlockSyntax markupAttribute:
                    attributeNames.Add(markupAttribute.Name.GetContent());
                    break;
 
                case MarkupMinimizedAttributeBlockSyntax minimizedMarkupAttribute:
                    attributeNames.Add(minimizedMarkupAttribute.Name.GetContent());
                    break;
 
                case MarkupTagHelperDirectiveAttributeSyntax directiveAttribute:
                    attributeNames.Add(directiveAttribute.FullName);
                    break;
 
                case MarkupMinimizedTagHelperDirectiveAttributeSyntax minimizedDirectiveAttribute:
                    attributeNames.Add(minimizedDirectiveAttribute.FullName);
                    break;
            }
        }
 
        return attributeNames.ToImmutableAndClear();
    }
 
    private static void SplitAttributeNameIntoParts(
        string attributeName,
        TextSpan attributeNameLocation,
        out string name,
        out TextSpan nameLocation,
        out string? parameterName,
        out TextSpan parameterLocation)
    {
        name = attributeName;
        nameLocation = attributeNameLocation;
        parameterName = null;
        parameterLocation = default;
 
        // It's possible that the attribute looks like a directive attribute but is incomplete.
        // We should try and extract out the transition and parameter.
 
        if (attributeName.Length == 0 || attributeName[0] != '@')
        {
            // Doesn't look like a directive attribute. Not an incomplete directive attribute.
            return;
        }
 
        var colonIndex = attributeName.IndexOf(':');
        if (colonIndex == -1)
        {
            // There's no parameter, the existing attribute name and location is sufficient.
            return;
        }
 
        name = attributeName[..colonIndex];
        nameLocation = new TextSpan(attributeNameLocation.Start, name.Length);
        parameterName = attributeName[(colonIndex + 1)..];
        parameterLocation = new TextSpan(attributeNameLocation.Start + colonIndex + 1, parameterName.Length);
    }
}