Skip to content

Commit

Permalink
PR feedback and move unindent logic to FormattingUtilities
Browse files Browse the repository at this point in the history
  • Loading branch information
ryzngard committed Oct 18, 2024
1 parent 94407a0 commit d64e2b3
Show file tree
Hide file tree
Showing 4 changed files with 161 additions and 77 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
using Microsoft.AspNetCore.Razor.Utilities;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Razor;
using Microsoft.CodeAnalysis.Razor.Formatting;
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
using Microsoft.CodeAnalysis.Razor.Workspaces;
using Microsoft.CodeAnalysis.Text;
Expand Down Expand Up @@ -93,21 +94,8 @@ internal sealed class ExtractToComponentCodeActionResolver(
builder.AppendLine();
}

var indentation = GetIndentation(actionParams.Start, text);
var extractedText = text.GetSubTextString(TextSpan.FromBounds(actionParams.Start, actionParams.End));
var lines = extractedText.Split('\n');
for (var i = 0; i < lines.Length; i++)
{
var line = UnindentLine(lines[i], indentation);
if (i == (lines.Length - 1))
{
builder.Append(line);
}
else
{
builder.Append(line + '\n');
}
}
var span = TextSpan.FromBounds(actionParams.Start, actionParams.End);
FormattingUtilities.NaivelyUnindentSubstring(text, span, builder);

var removeRange = text.GetRange(actionParams.Start, actionParams.End);

Expand Down Expand Up @@ -145,33 +133,4 @@ internal sealed class ExtractToComponentCodeActionResolver(
DocumentChanges = documentChanges,
};
}

private string UnindentLine(string line, int indentation)
{
var startCharacter = 0;

// Keep passing characters until either we reach the root indendation level
// or we would consume a character that isn't whitespace. This does make assumptions
// about consistency of tabs or spaces but at least will only fail to unindent correctly
while (startCharacter < indentation && IsWhitespace(line[startCharacter]))
{
startCharacter++;
}

return line[startCharacter..];
}

private int GetIndentation(int start, SourceText text)
{
var dedent = 0;
while (IsWhitespace(text[--start]))
{
dedent++;
}

return dedent;
}

private static bool IsWhitespace(char c)
=> c == ' ' || c == '\t';
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
// 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.Collections.Generic;

Check failure on line 5 in src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/FormattingUtilities.cs

View check run for this annotation

Azure Pipelines / razor-tooling-ci (Build macOS debug)

src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/FormattingUtilities.cs#L5

src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/FormattingUtilities.cs(5,1): error IDE0005: (NETCORE_ENGINEERING_TELEMETRY=Build) Using directive is unnecessary. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0005)

Check failure on line 5 in src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/FormattingUtilities.cs

View check run for this annotation

Azure Pipelines / razor-tooling-ci (Build macOS debug)

src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/FormattingUtilities.cs#L5

src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/FormattingUtilities.cs(5,1): error IDE0005: (NETCORE_ENGINEERING_TELEMETRY=Build) Using directive is unnecessary. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0005)

Check failure on line 5 in src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/FormattingUtilities.cs

View check run for this annotation

Azure Pipelines / razor-tooling-ci (Build macOS debug)

src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/FormattingUtilities.cs#L5

src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/FormattingUtilities.cs(5,1): error IDE0005: (NETCORE_ENGINEERING_TELEMETRY=Build) Using directive is unnecessary. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0005)

Check failure on line 5 in src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/FormattingUtilities.cs

View check run for this annotation

Azure Pipelines / razor-tooling-ci (Build macOS release)

src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/FormattingUtilities.cs#L5

src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/FormattingUtilities.cs(5,1): error IDE0005: (NETCORE_ENGINEERING_TELEMETRY=Build) Using directive is unnecessary. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0005)

Check failure on line 5 in src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/FormattingUtilities.cs

View check run for this annotation

Azure Pipelines / razor-tooling-ci (Build macOS release)

src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/FormattingUtilities.cs#L5

src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/FormattingUtilities.cs(5,1): error IDE0005: (NETCORE_ENGINEERING_TELEMETRY=Build) Using directive is unnecessary. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0005)

Check failure on line 5 in src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/FormattingUtilities.cs

View check run for this annotation

Azure Pipelines / razor-tooling-ci (Build macOS release)

src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/FormattingUtilities.cs#L5

src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/FormattingUtilities.cs(5,1): error IDE0005: (NETCORE_ENGINEERING_TELEMETRY=Build) Using directive is unnecessary. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0005)

Check failure on line 5 in src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/FormattingUtilities.cs

View check run for this annotation

Azure Pipelines / razor-tooling-ci (Build Linux debug)

src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/FormattingUtilities.cs#L5

src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/FormattingUtilities.cs(5,1): error IDE0005: (NETCORE_ENGINEERING_TELEMETRY=Build) Using directive is unnecessary. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0005)

Check failure on line 5 in src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/FormattingUtilities.cs

View check run for this annotation

Azure Pipelines / razor-tooling-ci (Build Linux debug)

src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/FormattingUtilities.cs#L5

src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/FormattingUtilities.cs(5,1): error IDE0005: (NETCORE_ENGINEERING_TELEMETRY=Build) Using directive is unnecessary. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0005)

Check failure on line 5 in src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/FormattingUtilities.cs

View check run for this annotation

Azure Pipelines / razor-tooling-ci (Build Linux debug)

src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/FormattingUtilities.cs#L5

src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/FormattingUtilities.cs(5,1): error IDE0005: (NETCORE_ENGINEERING_TELEMETRY=Build) Using directive is unnecessary. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0005)

Check failure on line 5 in src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/FormattingUtilities.cs

View check run for this annotation

Azure Pipelines / razor-tooling-ci (Build Linux release)

src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/FormattingUtilities.cs#L5

src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/FormattingUtilities.cs(5,1): error IDE0005: (NETCORE_ENGINEERING_TELEMETRY=Build) Using directive is unnecessary. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0005)

Check failure on line 5 in src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/FormattingUtilities.cs

View check run for this annotation

Azure Pipelines / razor-tooling-ci (Build Linux release)

src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/FormattingUtilities.cs#L5

src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/FormattingUtilities.cs(5,1): error IDE0005: (NETCORE_ENGINEERING_TELEMETRY=Build) Using directive is unnecessary. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0005)

Check failure on line 5 in src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/FormattingUtilities.cs

View check run for this annotation

Azure Pipelines / razor-tooling-ci (Build Linux release)

src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/FormattingUtilities.cs#L5

src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/FormattingUtilities.cs(5,1): error IDE0005: (NETCORE_ENGINEERING_TELEMETRY=Build) Using directive is unnecessary. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0005)
using System.Collections.Immutable;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.PooledObjects;
using Microsoft.CodeAnalysis.Text;
using Roslyn.LanguageServer.Protocol;

namespace Microsoft.CodeAnalysis.Razor.Formatting;

Expand Down Expand Up @@ -136,4 +142,123 @@ public static string GetIndentationString(int indentation, bool insertSpaces, in
var combined = string.Concat(tabPrefix, spaceSuffix);
return combined;
}

/// <summary>
/// Unindents a span of text with a few caveats:
///
/// 1. This assumes consistency in tabs/spaces for starting whitespace per line
/// 2. This doesn't format the text, just attempts to remove leading whitespace in a uniform way
/// 3. It will never remove non-whitespace
///
/// This was made with extracting code into a new file in mind because it's not trivial to format that text and make
/// sure the indentation is right. Use with caution.
/// </summary>
public static void NaivelyUnindentSubstring(SourceText text, TextSpan extractionSpan, System.Text.StringBuilder builder)
{
var extractedText = text.GetSubTextString(extractionSpan);
var range = text.GetRange(extractionSpan);
if (range.Start.Line == range.End.Line)
{
builder.Append(extractedText);
return;
}

var extractedTextSpan = extractedText.AsSpan();
var indentation = GetNormalizedIndentation(text, extractionSpan);

foreach (var lineRange in GetLineRanges(extractedText))
{
var lineSpan = extractedTextSpan[lineRange];
lineSpan = UnindentLine(lineSpan, indentation);

foreach (var c in lineSpan)
{
builder.Append(c);
}
}

//
// Local Methods
//

static ReadOnlySpan<char> UnindentLine(ReadOnlySpan<char> line, int indentation)
{
var startCharacter = 0;
while (startCharacter < indentation && IsWhitespace(line[startCharacter]))
{
startCharacter++;
}

return line[startCharacter..];
}

// Gets the smallest indentation of all the lines in a given span
static int GetNormalizedIndentation(SourceText sourceText, TextSpan span)
{
var indentation = int.MaxValue;
foreach (var line in sourceText.Lines)
{
if (!span.OverlapsWith(line.Span))
{
continue;
}

indentation = Math.Min(indentation, GetIndentation(line));
}

return indentation;
}

static int GetIndentation(TextLine line)
{
if (line.Text is null)
{
return 0;
}

var indentation = 0;
for (var position = line.Span.Start; position < line.Span.End; position++)
{
var c = line.Text[position];
if (!IsWhitespace(c))
{
break;
}

indentation++;
}

return indentation;
}

static bool IsWhitespace(char c)
=> c == ' ' || c == '\t';

static ImmutableArray<System.Range> GetLineRanges(string text)
{
using var builder = new PooledArrayBuilder<System.Range>();
var start = 0;
var end = text.IndexOf('\n');
while (true)
{
if (end == -1)
{
builder.Add(new(start, text.Length));
break;
}

// end + 1 to include the new line
builder.Add(new(start, end + 1));
start = end + 1;
if (start == text.Length)
{
break;
}

end = text.IndexOf('\n', start);
}

return builder.DrainToImmutable();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1156,7 +1156,7 @@ namespace MarketApp.Pages.Product.Home
<div>
<div>
<div>
<h1>[|selection:Div a title</h1>
<h1>[|Div a title</h1>
<p>Div a par</p>
</div>
</div>
Expand All @@ -1177,40 +1177,40 @@ namespace MarketApp.Pages.Product.Home
""";

var expectedRazorComponent = """
<div>
<div>
<div>
<h1>selection:Div a title</h1>
<p>Div a par</p>
<div>
<h1>Div a title</h1>
<p>Div a par</p>
</div>
</div>
</div>
</div>
<div>
<div>
<div>
<h1>Div b title</h1>
<p>Div b par</p>
<div>
<h1>Div b title</h1>
<p>Div b par</p>
</div>
</div>
</div>
</div>
""";
""";

var expectedOriginalDocument = """
@page "/"
@namespace MarketApp.Pages.Product.Home
@page "/"
@namespace MarketApp.Pages.Product.Home
namespace MarketApp.Pages.Product.Home
namespace MarketApp.Pages.Product.Home
<PageTitle>Home</PageTitle>
<PageTitle>Home</PageTitle>
<div id="parent">
<Component />
</div>
<div id="parent">
<Component />
</div>
<h1>Hello, world!</h1>
<h1>Hello, world!</h1>
Welcome to your new app.
""";
Welcome to your new app.
""";

await ValidateExtractComponentCodeActionAsync(
input,
Expand All @@ -1225,32 +1225,32 @@ await ValidateExtractComponentCodeActionAsync(
public async Task Handle_ExtractComponent_StartNodeContainsEndNode()
{
var input = """
< [|div id="parent">
<div>
<[|div id="parent">
<div>
<div>
<p>Deeply nested par</p|]>
<div>
<p>Deeply nested par</p|]>
</div>
</div>
</div>
</div>
</div>
""";
""";

var expectedRazorComponent = """
<div id="parent">
<div>
<div id="parent">
<div>
<div>
<p>Deeply nested par</p>
<div>
<p>Deeply nested par</p>
</div>
</div>
</div>
</div>
</div>
""";
""";

var expectedOriginalDocument = """
<Component />
""";
<Component />
""";

await ValidateExtractComponentCodeActionAsync(
input,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -384,7 +384,7 @@ public Task Handle_MultipointSelection_CSharpBlock()
{|result:<div{|selection:>blah</div>
@{
RenderFragment fragment = @<Component1 Id="Comp1" Caption="Title">|] </Component1>;
RenderFragment fragment = @<Component1 Id="Comp1" Caption="Title">|} </Component1>;
}|}
""");

Expand Down

0 comments on commit d64e2b3

Please sign in to comment.