Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Code action to offer to wrap Html attributes #11422

Merged
merged 2 commits into from
Feb 4, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,8 @@ public static void AddCodeActionsServices(this IServiceCollection services)
services.AddSingleton<IRazorCodeActionResolver, GenerateMethodCodeActionResolver>();
services.AddSingleton<IRazorCodeActionProvider, PromoteUsingCodeActionProvider>();
services.AddSingleton<IRazorCodeActionResolver, PromoteUsingCodeActionResolver>();
services.AddSingleton<IRazorCodeActionProvider, WrapAttributesCodeActionProvider>();
services.AddSingleton<IRazorCodeActionResolver, WrapAttributesCodeActionResolver>();

// Html Code actions
services.AddSingleton<IHtmlCodeActionProvider, HtmlCodeActionProvider>();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT license. See License.txt in the project root for license information.

using System.Text.Json.Serialization;

namespace Microsoft.CodeAnalysis.Razor.CodeActions.Models;

internal sealed class WrapAttributesCodeActionParams
{
[JsonPropertyName("indentSize")]
public int IndentSize { get; init; }

[JsonPropertyName("newLinePositions")]
public required int[] NewLinePositions { get; init; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,16 @@ internal static class RazorCodeActionFactory
private readonly static Guid s_generateMethodTelemetryId = new("c14fa003-c752-45fc-bb29-3a123ae5ecef");
private readonly static Guid s_generateAsyncMethodTelemetryId = new("9058ca47-98e2-4f11-bf7c-a16a444dd939");
private readonly static Guid s_promoteUsingDirectiveTelemetryId = new("751f9012-e37b-444a-9211-b4ebce91d96e");
private readonly static Guid s_wrapAttributesTelemetryId = new("1df50ba6-4ed1-40d8-8fe2-1c4c1b08e8b5");

public static RazorVSInternalCodeAction CreateWrapAttributes(RazorCodeActionResolutionParams resolutionParams)
=> new RazorVSInternalCodeAction
{
Title = SR.Wrap_attributes,
Data = JsonSerializer.SerializeToElement(resolutionParams),
TelemetryId = s_wrapAttributesTelemetryId,
Name = LanguageServerConstants.CodeActions.WrapAttributes,
};

public static RazorVSInternalCodeAction CreatePromoteUsingDirective(string importsFileName, RazorCodeActionResolutionParams resolutionParams)
=> new RazorVSInternalCodeAction
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT license. See License.txt in the project root for license information.

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>();
}

var syntaxTree = context.CodeDocument.GetSyntaxTree();
if (syntaxTree?.Root is null)
{
return SpecializedTasks.EmptyImmutableArray<RazorVSInternalCodeAction>();
}

var owner = syntaxTree.Root.FindNode(TextSpan.FromBounds(context.StartAbsoluteIndex, context.EndAbsoluteIndex));
if (owner is null)
{
return SpecializedTasks.EmptyImmutableArray<RazorVSInternalCodeAction>();
}

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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TryGetFirstNonWhitespaceOffset

Are there cases when attribute span starts with whitespace?

Copy link
Contributor Author

@davidwengier davidwengier Feb 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, the Razor syntax tree continues to be annoying, so (almost?) all of the attributes start with whitespace. The syntax nodes look a bit like[<][foo][ class="asdf"][/>].

indentSize = linePositionSpan.Start.Character + indentSizeOffset;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

indentSizeOffset

indentSizeOffset would be zero in most cases but not always?

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 AspNetCore.Razor.Language.Syntax.SyntaxList<RazorSyntaxNode> FindAttributes(AspNetCore.Razor.Language.Syntax.SyntaxNode owner)
{
foreach (var node in owner.AncestorsAndSelf())
davidwengier marked this conversation as resolved.
Show resolved Hide resolved
{
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 [];
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT license. See License.txt in the project root for license information.

using System;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.PooledObjects;
using Microsoft.CodeAnalysis.Razor.CodeActions.Models;
using Microsoft.CodeAnalysis.Razor.Formatting;
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
using Microsoft.CodeAnalysis.Razor.Protocol;
using Microsoft.CodeAnalysis.Text;
using Microsoft.VisualStudio.LanguageServer.Protocol;

namespace Microsoft.CodeAnalysis.Razor.CodeActions;

internal class WrapAttributesCodeActionResolver : IRazorCodeActionResolver
{
public string Action => LanguageServerConstants.CodeActions.WrapAttributes;

public async Task<WorkspaceEdit?> ResolveAsync(DocumentContext documentContext, JsonElement data, RazorFormattingOptions options, CancellationToken cancellationToken)
{
var actionParams = data.Deserialize<WrapAttributesCodeActionParams>();
if (actionParams is null)
{
return null;
}

var indentationString = FormattingUtilities.GetIndentationString(actionParams.IndentSize, options.InsertSpaces, options.TabSize);
var sourceText = await documentContext.GetSourceTextAsync(cancellationToken).ConfigureAwait(false);
using var edits = new PooledArrayBuilder<TextEdit>();

foreach (var position in actionParams.NewLinePositions)
{
var start = sourceText.GetLinePosition(FindPreviousNonWhitespacePosition(sourceText, position) + 1);
var end = sourceText.GetLinePosition(position);
edits.Add(VsLspFactory.CreateTextEdit(start, end, Environment.NewLine + indentationString));
}

var tde = new TextDocumentEdit
{
TextDocument = new OptionalVersionedTextDocumentIdentifier() { Uri = documentContext.Uri },
Edits = edits.ToArray()
};

return new WorkspaceEdit
{
DocumentChanges = new SumType<TextDocumentEdit[], SumType<TextDocumentEdit, CreateFile, RenameFile, DeleteFile>[]>([tde])
};
}

private int FindPreviousNonWhitespacePosition(SourceText sourceText, int position)
{
for (var i = position - 1; i >= 0; i--)
{
if (!char.IsWhiteSpace(sourceText[i]))
{
return i;
}
}

return 0;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ public static class CodeActions

public const string CodeActionFromVSCode = "CodeActionFromVSCode";

public const string WrapAttributes = "WrapAttributes";

/// <summary>
/// Remaps without formatting the resolved code action edit
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -196,4 +196,7 @@
<data name="Promote_using_directive_to" xml:space="preserve">
<value>Promote using directive to {0}</value>
</data>
<data name="Wrap_attributes" xml:space="preserve">
<value>Wrap attributes</value>
</data>
</root>

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading