diff --git a/src/Microsoft.AspNet.Razor/Generator/Compiler/CodeBuilder/CSharp/CSharpPaddingBuilder.cs b/src/Microsoft.AspNet.Razor/Generator/Compiler/CodeBuilder/CSharp/CSharpPaddingBuilder.cs new file mode 100644 index 0000000000..774600b6eb --- /dev/null +++ b/src/Microsoft.AspNet.Razor/Generator/Compiler/CodeBuilder/CSharp/CSharpPaddingBuilder.cs @@ -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: + // : + // 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; + } + } +} diff --git a/src/Microsoft.AspNet.Razor/Generator/Compiler/CodeBuilder/CSharp/Visitors/CSharpCodeVisitor.cs b/src/Microsoft.AspNet.Razor/Generator/Compiler/CodeBuilder/CSharp/Visitors/CSharpCodeVisitor.cs index b73779744d..3355e26685 100644 --- a/src/Microsoft.AspNet.Razor/Generator/Compiler/CodeBuilder/CSharp/Visitors/CSharpCodeVisitor.cs +++ b/src/Microsoft.AspNet.Razor/Generator/Compiler/CodeBuilder/CSharp/Visitors/CSharpCodeVisitor.cs @@ -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(); + } + } } } diff --git a/test/Microsoft.AspNet.Razor.Test/Generator/CodeTree/CSharpPaddingBuilderTests.cs b/test/Microsoft.AspNet.Razor.Test/Generator/CodeTree/CSharpPaddingBuilderTests.cs new file mode 100644 index 0000000000..520b9e6e38 --- /dev/null +++ b/test/Microsoft.AspNet.Razor.Test/Generator/CodeTree/CSharpPaddingBuilderTests.cs @@ -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
\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
\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\r\n\r\n\t\t@if (true) { \r\n\r\n"; + + 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 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; + } + } +} diff --git a/test/Microsoft.AspNet.Razor.Test/Microsoft.AspNet.Razor.Test.csproj b/test/Microsoft.AspNet.Razor.Test/Microsoft.AspNet.Razor.Test.csproj index 8fe9209114..5b55bdbe8a 100644 --- a/test/Microsoft.AspNet.Razor.Test/Microsoft.AspNet.Razor.Test.csproj +++ b/test/Microsoft.AspNet.Razor.Test/Microsoft.AspNet.Razor.Test.csproj @@ -52,6 +52,7 @@ +