File: CodeActions\Razor\WrapAttributesCodeActionProvider.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.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.Language.Syntax;
using Microsoft.AspNetCore.Razor.PooledObjects;
using Microsoft.AspNetCore.Razor.Threading;
using Microsoft.CodeAnalysis.Razor.CodeActions.Models;
using Microsoft.CodeAnalysis.Razor.CodeActions.Razor;
using Microsoft.CodeAnalysis.Razor.Protocol;
using Microsoft.CodeAnalysis.Text;
 
namespace Microsoft.CodeAnalysis.Razor.CodeActions;
 
internal class WrapAttributesCodeActionProvider : IRazorCodeActionProvider
{
    public Task<ImmutableArray<RazorVSInternalCodeAction>> ProvideAsync(RazorCodeActionContext context, CancellationToken cancellationToken)
    {
        if (context.HasSelection)
        {
            return SpecializedTasks.EmptyImmutableArray<RazorVSInternalCodeAction>();
        }
 
        if (!context.CodeDocument.TryGetSyntaxRoot(out var root))
        {
            return SpecializedTasks.EmptyImmutableArray<RazorVSInternalCodeAction>();
        }
 
        var owner = root.FindNode(TextSpan.FromBounds(context.StartAbsoluteIndex, context.EndAbsoluteIndex));
        var attributes = FindAttributes(owner);
        if (attributes.Count == 0)
        {
            return SpecializedTasks.EmptyImmutableArray<RazorVSInternalCodeAction>();
        }
 
        var first = true;
        var firstAttributeLine = 0;
        var indentSize = 0;
        var sourceText = context.SourceText;
 
        using var newLinePositions = new PooledArrayBuilder<int>(attributes.Count);
        foreach (var attribute in attributes)
        {
            var linePositionSpan = attribute.GetLinePositionSpan(context.CodeDocument.Source);
 
            if (first)
            {
                firstAttributeLine = linePositionSpan.Start.Line;
                sourceText.TryGetFirstNonWhitespaceOffset(attribute.Span, out var indentSizeOffset);
                indentSize = linePositionSpan.Start.Character + indentSizeOffset;
                first = false;
            }
            else
            {
                if (linePositionSpan.Start.Line != firstAttributeLine)
                {
                    return SpecializedTasks.EmptyImmutableArray<RazorVSInternalCodeAction>();
                }
 
                if (!sourceText.TryGetFirstNonWhitespaceOffset(attribute.Span, out var startOffset))
                {
                    continue;
                }
 
                newLinePositions.Add(attribute.SpanStart + startOffset);
            }
        }
 
        if (newLinePositions.Count == 0)
        {
            return SpecializedTasks.EmptyImmutableArray<RazorVSInternalCodeAction>();
        }
 
        var data = new WrapAttributesCodeActionParams
        {
            IndentSize = indentSize,
            NewLinePositions = newLinePositions.ToArray()
        };
 
        var resolutionParams = new RazorCodeActionResolutionParams()
        {
            TextDocument = context.Request.TextDocument,
            Action = LanguageServerConstants.CodeActions.WrapAttributes,
            Language = RazorLanguageKind.Razor,
            DelegatedDocumentUri = context.DelegatedDocumentUri,
            Data = data
        };
 
        var action = RazorCodeActionFactory.CreateWrapAttributes(resolutionParams);
 
        return Task.FromResult<ImmutableArray<RazorVSInternalCodeAction>>([action]);
    }
 
    private static AspNetCore.Razor.Language.Syntax.SyntaxList<RazorSyntaxNode> FindAttributes(AspNetCore.Razor.Language.Syntax.SyntaxNode? owner)
    {
        // Sometimes FindNode will find the start tag, sometimes the element. We always start from the start tag to make searching
        // easier, and since we are concerned with attributes, things without start tags wouldn't be applicalbe anyway
        if (owner is MarkupElementSyntax element)
        {
            owner = element.StartTag;
        }
        else if (owner is MarkupTagHelperElementSyntax tagHelperElement)
        {
            owner = tagHelperElement.StartTag;
        }
 
        if (owner is null)
        {
            return [];
        }
 
        foreach (var node in owner.AncestorsAndSelf())
        {
            if (node is MarkupStartTagSyntax startTag)
            {
                return startTag.Attributes;
            }
            else if (node is MarkupTagHelperStartTagSyntax tagHelperElement)
            {
                return tagHelperElement.Attributes;
            }
            else if (node is MarkupElementSyntax or MarkupTagHelperElementSyntax)
            {
                // If we get as high as the element, we're done looking
                break;
            }
        }
 
        return [];
    }
}