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:
N. Taylor Mullen 2014-02-11 14:46:17 -08:00
parent 63e55ce776
commit 8db45f7564
4 changed files with 425 additions and 10 deletions

View File

@ -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;
}
}
}

View File

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

View File

@ -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;
}
}
}

View File

@ -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" />