Add user-based padding support.
Added a CSharpPaddingBuilder based on the existing CodeGeneratorPaddingHelper to allow accurate padding within the generated C# files. Also created tests based on the existing PaddingTest tests to verify padding functionality.
This commit is contained in:
parent
63e55ce776
commit
8db45f7564
|
|
@ -0,0 +1,149 @@
|
|||
using System;
|
||||
using Microsoft.AspNet.Razor.Parser;
|
||||
using Microsoft.AspNet.Razor.Parser.SyntaxTree;
|
||||
|
||||
namespace Microsoft.AspNet.Razor.Generator.Compiler.CSharp
|
||||
{
|
||||
public class CSharpPaddingBuilder
|
||||
{
|
||||
private static readonly char[] _newLineChars = { '\r', '\n' };
|
||||
|
||||
private RazorEngineHost _host;
|
||||
|
||||
public CSharpPaddingBuilder(RazorEngineHost host)
|
||||
{
|
||||
_host = host;
|
||||
}
|
||||
|
||||
// Special case for statement padding to account for brace positioning in the editor.
|
||||
public string BuildStatementPadding(Span target)
|
||||
{
|
||||
if (target == null)
|
||||
{
|
||||
throw new ArgumentNullException("target");
|
||||
}
|
||||
|
||||
int padding = CalculatePadding(target);
|
||||
|
||||
// We treat statement padding specially so for brace positioning, so that in the following example:
|
||||
// @if (foo > 0)
|
||||
// {
|
||||
// }
|
||||
//
|
||||
// the braces shows up under the @ rather than under the if.
|
||||
if (_host.DesignTimeMode &&
|
||||
padding > 0 &&
|
||||
target.Previous.Kind == SpanKind.Transition && // target.Previous is guaranteed to not be null if you have padding.
|
||||
String.Equals(target.Previous.Content, SyntaxConstants.TransitionString, StringComparison.Ordinal))
|
||||
{
|
||||
padding--;
|
||||
}
|
||||
|
||||
string generatedCode = BuildPaddingInternal(padding);
|
||||
|
||||
return generatedCode;
|
||||
}
|
||||
|
||||
public string BuildExpressionPadding(Span target)
|
||||
{
|
||||
int padding = CalculatePadding(target);
|
||||
|
||||
return BuildPaddingInternal(padding);
|
||||
}
|
||||
|
||||
internal int CalculatePadding(Span target)
|
||||
{
|
||||
if (target == null)
|
||||
{
|
||||
throw new ArgumentNullException("target");
|
||||
}
|
||||
|
||||
return CollectSpacesAndTabs(target, _host.TabSize);
|
||||
}
|
||||
|
||||
private string BuildPaddingInternal(int padding)
|
||||
{
|
||||
if (_host.DesignTimeMode && _host.IsIndentingWithTabs)
|
||||
{
|
||||
int spaces = padding % _host.TabSize;
|
||||
int tabs = padding / _host.TabSize;
|
||||
|
||||
return new string('\t', tabs) + new string(' ', spaces);
|
||||
}
|
||||
else
|
||||
{
|
||||
return new string(' ', padding);
|
||||
}
|
||||
}
|
||||
|
||||
private static int CollectSpacesAndTabs(Span target, int tabSize)
|
||||
{
|
||||
Span firstSpanInLine = target;
|
||||
|
||||
string currentContent = null;
|
||||
|
||||
while (firstSpanInLine.Previous != null)
|
||||
{
|
||||
// When scanning previous spans we need to be break down the spans with spaces. The parser combines
|
||||
// whitespace into existing spans so you'll see tabs, newlines etc. within spans. We only care about
|
||||
// the \t in existing spans.
|
||||
String previousContent = firstSpanInLine.Previous.Content ?? String.Empty;
|
||||
|
||||
int lastNewLineIndex = previousContent.LastIndexOfAny(_newLineChars);
|
||||
|
||||
if (lastNewLineIndex < 0)
|
||||
{
|
||||
firstSpanInLine = firstSpanInLine.Previous;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (lastNewLineIndex != previousContent.Length - 1)
|
||||
{
|
||||
firstSpanInLine = firstSpanInLine.Previous;
|
||||
currentContent = previousContent.Substring(lastNewLineIndex + 1);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// We need to walk from the beginning of the line, because space + tab(tabSize) = tabSize columns, but tab(tabSize) + space = tabSize+1 columns.
|
||||
Span currentSpanInLine = firstSpanInLine;
|
||||
|
||||
if (currentContent == null)
|
||||
{
|
||||
currentContent = currentSpanInLine.Content;
|
||||
}
|
||||
|
||||
int padding = 0;
|
||||
while (currentSpanInLine != target)
|
||||
{
|
||||
if (currentContent != null)
|
||||
{
|
||||
for (int i = 0; i < currentContent.Length; i++)
|
||||
{
|
||||
if (currentContent[i] == '\t')
|
||||
{
|
||||
// Example:
|
||||
// <space><space><tab><tab>:
|
||||
// iter 1) 1
|
||||
// iter 2) 2
|
||||
// iter 3) 4 = 2 + (4 - 2)
|
||||
// iter 4) 8 = 4 + (4 - 0)
|
||||
padding = padding + (tabSize - (padding % tabSize));
|
||||
}
|
||||
else
|
||||
{
|
||||
padding++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
currentSpanInLine = currentSpanInLine.Next;
|
||||
currentContent = currentSpanInLine.Content;
|
||||
}
|
||||
|
||||
return padding;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
using System;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNet.Razor.Parser.SyntaxTree;
|
||||
|
||||
namespace Microsoft.AspNet.Razor.Generator.Compiler.CSharp
|
||||
{
|
||||
|
|
@ -9,9 +10,12 @@ namespace Microsoft.AspNet.Razor.Generator.Compiler.CSharp
|
|||
private const string ValueWriterName = "__razor_attribute_value_writer";
|
||||
private const string TemplateWriterName = "__razor_template_writer";
|
||||
|
||||
private CSharpPaddingBuilder _paddingBuilder;
|
||||
|
||||
public CSharpCodeVisitor(CSharpCodeWriter writer, CodeGeneratorContext context)
|
||||
: base(writer, context)
|
||||
{
|
||||
_paddingBuilder = new CSharpPaddingBuilder(context.Host);
|
||||
}
|
||||
|
||||
protected override void Visit(SetLayoutChunk chunk)
|
||||
|
|
@ -152,20 +156,13 @@ namespace Microsoft.AspNet.Razor.Generator.Compiler.CSharp
|
|||
|
||||
protected override void Visit(ExpressionChunk chunk)
|
||||
{
|
||||
using (Writer.BuildLineMapping(chunk.Start, chunk.Code.Length, Context.SourceFile))
|
||||
{
|
||||
Writer.Indent(chunk.Start.CharacterIndex)
|
||||
.Write(chunk.Code);
|
||||
}
|
||||
CreateCodeMapping(chunk.Code, chunk);
|
||||
}
|
||||
|
||||
protected override void Visit(StatementChunk chunk)
|
||||
{
|
||||
using (Writer.BuildLineMapping(chunk.Start, chunk.Code.Length, Context.SourceFile))
|
||||
{
|
||||
Writer.Indent(chunk.Start.CharacterIndex);
|
||||
Writer.WriteLine(chunk.Code);
|
||||
}
|
||||
CreateCodeMapping(chunk.Code, chunk);
|
||||
Writer.WriteLine();
|
||||
}
|
||||
|
||||
protected override void Visit(DynamicCodeAttributeChunk chunk)
|
||||
|
|
@ -310,5 +307,17 @@ namespace Microsoft.AspNet.Razor.Generator.Compiler.CSharp
|
|||
|
||||
Writer.WriteEndMethodInvocation();
|
||||
}
|
||||
|
||||
private void CreateCodeMapping(string code, Chunk chunk)
|
||||
{
|
||||
using (CSharpLineMappingWriter mappingWriter = Writer.BuildLineMapping(chunk.Start, code.Length, Context.SourceFile))
|
||||
{
|
||||
Writer.Write(_paddingBuilder.BuildExpressionPadding((Span)chunk.Association));
|
||||
|
||||
mappingWriter.MarkLineMappingStart();
|
||||
Writer.Write(code);
|
||||
mappingWriter.MarkLineMappingEnd();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,256 @@
|
|||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNet.Razor.Generator.Compiler.CSharp;
|
||||
using Microsoft.AspNet.Razor.Parser;
|
||||
using Microsoft.AspNet.Razor.Parser.SyntaxTree;
|
||||
using Microsoft.TestCommon;
|
||||
|
||||
namespace Microsoft.AspNet.Razor.Test.Generator.CodeTree
|
||||
{
|
||||
public class CSharpPaddingBuilderTests
|
||||
{
|
||||
[Fact]
|
||||
public void CalculatePaddingForEmptySpanReturnsZero()
|
||||
{
|
||||
// Arrange
|
||||
RazorEngineHost host = CreateHost(designTime: true);
|
||||
|
||||
Span span = new Span(new SpanBuilder());
|
||||
|
||||
var paddingBuilder = new CSharpPaddingBuilder(host);
|
||||
|
||||
// Act
|
||||
int padding = paddingBuilder.CalculatePadding(span);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(0, padding);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[PropertyData("SpacePropertyData")]
|
||||
public void CalculatePaddingForEmptySpanWith4Spaces(bool designTime, bool isIndentingWithTabs, int tabSize)
|
||||
{
|
||||
// Arrange
|
||||
RazorEngineHost host = CreateHost(designTime, isIndentingWithTabs, tabSize);
|
||||
|
||||
Span span = GenerateSpan(@" @{", SpanKind.Code, 3, "");
|
||||
|
||||
var paddingBuilder = new CSharpPaddingBuilder(host);
|
||||
|
||||
// Act
|
||||
int padding = paddingBuilder.CalculatePadding(span);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(6, padding);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[PropertyData("SpacePropertyData")]
|
||||
public void CalculatePaddingForIfSpanWith5Spaces(bool designTime, bool isIndentingWithTabs, int tabSize)
|
||||
{
|
||||
// Arrange
|
||||
RazorEngineHost host = CreateHost(designTime, isIndentingWithTabs, tabSize);
|
||||
|
||||
Span span = GenerateSpan(@" @if (true)", SpanKind.Code, 2, "if (true)");
|
||||
|
||||
var paddingBuilder = new CSharpPaddingBuilder(host);
|
||||
|
||||
// Act
|
||||
int padding = paddingBuilder.CalculatePadding(span);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(5, padding);
|
||||
}
|
||||
|
||||
// 4 padding should result in 4 spaces. Where in the previous test (5 spaces) should result in 1 tab.
|
||||
[Theory]
|
||||
[InlineData(true, false, 4, 0, 4)]
|
||||
[InlineData(true, false, 2, 0, 4)]
|
||||
[InlineData(true, true, 4, 1, 0)]
|
||||
[InlineData(true, true, 2, 2, 0)]
|
||||
[InlineData(true, true, 1, 4, 0)]
|
||||
[InlineData(true, true, 0, 4, 0)]
|
||||
[InlineData(true, true, 3, 1, 1)]
|
||||
|
||||
// in non design time mode padding falls back to spaces to keep runtime code identical to v2 code.
|
||||
[InlineData(false, true, 4, 0, 5)]
|
||||
[InlineData(false, true, 2, 0, 5)]
|
||||
|
||||
[InlineData(false, false, 4, 0, 5)]
|
||||
[InlineData(false, false, 2, 0, 5)]
|
||||
public void VerifyPaddingForIfSpanWith4Spaces(bool designTime, bool isIndentingWithTabs, int tabSize, int numTabs, int numSpaces)
|
||||
{
|
||||
// Arrange
|
||||
RazorEngineHost host = CreateHost(designTime, isIndentingWithTabs, tabSize);
|
||||
|
||||
// no new lines involved
|
||||
Span spanFlat = GenerateSpan(" @if (true)", SpanKind.Code, 2, "if (true)");
|
||||
Span spanNewlines = GenerateSpan("\t<div>\r\n @if (true)", SpanKind.Code, 3, "if (true)");
|
||||
|
||||
var paddingBuilder = new CSharpPaddingBuilder(host);
|
||||
|
||||
// Act
|
||||
string paddingFlat = paddingBuilder.BuildStatementPadding(spanFlat);
|
||||
|
||||
|
||||
string paddingNewlines = paddingBuilder.BuildStatementPadding(spanNewlines);
|
||||
|
||||
// Assert
|
||||
string code = " if (true)";
|
||||
VerifyPadded(numTabs, numSpaces, code, paddingFlat);
|
||||
VerifyPadded(numTabs, numSpaces, code, paddingNewlines);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(true, false, 4, 0, 8)]
|
||||
[InlineData(true, false, 2, 0, 4)]
|
||||
[InlineData(true, true, 4, 2, 0)]
|
||||
[InlineData(true, true, 2, 2, 0)]
|
||||
[InlineData(true, true, 1, 2, 0)]
|
||||
[InlineData(true, true, 0, 2, 0)]
|
||||
[InlineData(true, true, 3, 2, 0)]
|
||||
|
||||
// in non design time mode padding falls back to spaces to keep runtime code identical to v2 code.
|
||||
[InlineData(false, true, 4, 0, 9)]
|
||||
[InlineData(false, true, 2, 0, 5)]
|
||||
|
||||
[InlineData(false, false, 4, 0, 9)]
|
||||
[InlineData(false, false, 2, 0, 5)]
|
||||
public void VerifyPaddingForIfSpanWithTwoTabs(bool designTime, bool isIndentingWithTabs, int tabSize, int numTabs, int numSpaces)
|
||||
{
|
||||
// Arrange
|
||||
RazorEngineHost host = CreateHost(designTime, isIndentingWithTabs, tabSize);
|
||||
|
||||
// no new lines involved
|
||||
Span spanFlat = GenerateSpan("\t\t@if (true)", SpanKind.Code, 2, "if (true)");
|
||||
Span spanNewlines = GenerateSpan("\t<div>\r\n\t\t@if (true)", SpanKind.Code, 3, "if (true)");
|
||||
|
||||
var paddingBuilder = new CSharpPaddingBuilder(host);
|
||||
|
||||
// Act
|
||||
string paddingFlat = paddingBuilder.BuildStatementPadding(spanFlat);
|
||||
string paddingNewlines = paddingBuilder.BuildStatementPadding(spanNewlines);
|
||||
|
||||
// Assert
|
||||
string code = " if (true)";
|
||||
VerifyPadded(numTabs, numSpaces, code, paddingFlat);
|
||||
VerifyPadded(numTabs, numSpaces, code, paddingNewlines);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(true, false, 4, 0, 8)]
|
||||
[InlineData(true, false, 2, 0, 4)]
|
||||
[InlineData(true, true, 4, 2, 0)]
|
||||
[InlineData(true, true, 2, 2, 0)]
|
||||
[InlineData(true, true, 1, 2, 0)]
|
||||
[InlineData(true, true, 0, 2, 0)]
|
||||
|
||||
// in non design time mode padding falls back to spaces to keep runtime code identical to v2 code.
|
||||
[InlineData(false, true, 4, 0, 9)]
|
||||
[InlineData(false, true, 2, 0, 5)]
|
||||
|
||||
[InlineData(false, false, 4, 0, 9)]
|
||||
[InlineData(false, false, 2, 0, 5)]
|
||||
public void CalculatePaddingForOpenedIf(bool designTime, bool isIndentingWithTabs, int tabSize, int numTabs, int numSpaces)
|
||||
{
|
||||
// Arrange
|
||||
RazorEngineHost host = CreateHost(designTime, isIndentingWithTabs, tabSize);
|
||||
|
||||
string text = "\r\n<html>\r\n<body>\r\n\t\t@if (true) { \r\n</body>\r\n</html>";
|
||||
|
||||
Span span = GenerateSpan(text, SpanKind.Code, 3, "if (true) { \r\n");
|
||||
|
||||
var paddingBuilder = new CSharpPaddingBuilder(host);
|
||||
|
||||
// Act
|
||||
string padding = paddingBuilder.BuildStatementPadding(span);
|
||||
|
||||
// Assert
|
||||
string code = " if (true) { \r\n";
|
||||
|
||||
VerifyPadded(numTabs, numSpaces, code, padding);
|
||||
}
|
||||
|
||||
private static void VerifyPadded(int numTabs, int numSpaces, string code, string padding)
|
||||
{
|
||||
string padded = padding + code;
|
||||
string expectedPadding = new string('\t', numTabs) + new string(' ', numSpaces);
|
||||
|
||||
Assert.Equal(expectedPadding, padding);
|
||||
Assert.Equal(numTabs + numSpaces + code.Length, padded.Length);
|
||||
|
||||
if (numTabs > 0 || numSpaces > 0)
|
||||
{
|
||||
Assert.True(padded.Length > numTabs + numSpaces, "padded string too short");
|
||||
}
|
||||
|
||||
for (int i = 0; i < numTabs; i++)
|
||||
{
|
||||
Assert.Equal('\t', padded[i]);
|
||||
}
|
||||
|
||||
for (int i = numTabs; i < numTabs + numSpaces; i++)
|
||||
{
|
||||
Assert.Equal(' ', padded[i]);
|
||||
}
|
||||
|
||||
Assert.Equal(numSpaces + numTabs, padding.Length);
|
||||
}
|
||||
|
||||
public static IEnumerable<object[]> SpacePropertyData
|
||||
{
|
||||
get
|
||||
{
|
||||
yield return new object[] { true, false, 4 };
|
||||
yield return new object[] { true, false, 2 };
|
||||
yield return new object[] { false, true, 4 };
|
||||
yield return new object[] { false, true, 2 };
|
||||
yield return new object[] { false, false, 4 };
|
||||
yield return new object[] { false, false, 2 };
|
||||
yield return new object[] { true, true, 4 };
|
||||
yield return new object[] { true, true, 2 };
|
||||
yield return new object[] { true, true, 1 };
|
||||
yield return new object[] { true, true, 0 };
|
||||
}
|
||||
}
|
||||
|
||||
private static RazorEngineHost CreateHost(bool designTime, bool isIndentingWithTabs = false, int tabSize = 4)
|
||||
{
|
||||
return new RazorEngineHost(new CSharpRazorCodeLanguage())
|
||||
{
|
||||
DesignTimeMode = designTime,
|
||||
IsIndentingWithTabs = isIndentingWithTabs,
|
||||
TabSize = tabSize,
|
||||
};
|
||||
}
|
||||
|
||||
private static Span GenerateSpan(string text, SpanKind spanKind, int spanIndex, string spanText)
|
||||
{
|
||||
Span[] spans = GenerateSpans(text, spanKind, spanIndex, spanText);
|
||||
|
||||
return spans[spanIndex];
|
||||
}
|
||||
|
||||
private static Span[] GenerateSpans(string text, SpanKind spanKind, int spanIndex, string spanText)
|
||||
{
|
||||
Assert.True(spanIndex > 0);
|
||||
|
||||
var parser = new RazorParser(new CSharpCodeParser(), new HtmlMarkupParser());
|
||||
|
||||
Span[] spans;
|
||||
|
||||
using (var reader = new StringReader(text))
|
||||
{
|
||||
ParserResults results = parser.Parse(reader);
|
||||
spans = results.Document.Flatten().ToArray();
|
||||
}
|
||||
|
||||
Assert.True(spans.Length > spanIndex);
|
||||
Assert.Equal(spanKind, spans[spanIndex].Kind);
|
||||
Assert.Equal(spanText, spans[spanIndex].Content);
|
||||
|
||||
return spans;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -52,6 +52,7 @@
|
|||
<Compile Include="Generator\CodeTree\CodeTreeGenerationTest.cs" />
|
||||
<Compile Include="Generator\CodeTree\CodeTreeOutputValidator.cs" />
|
||||
<Compile Include="Generator\CodeTree\CSharpCodeBuilderTests.cs" />
|
||||
<Compile Include="Generator\CodeTree\CSharpPaddingBuilderTests.cs" />
|
||||
<Compile Include="Generator\GeneratedCodeMappingTest.cs" />
|
||||
<Compile Include="Generator\PaddingTest.cs" />
|
||||
<Compile Include="Generator\TabTest.cs" />
|
||||
|
|
|
|||
Loading…
Reference in New Issue