diff --git a/src/Microsoft.AspNetCore.Razor.Language/Legacy/CSharpCodeParser.cs b/src/Microsoft.AspNetCore.Razor.Language/Legacy/CSharpCodeParser.cs index 621bfff401..b95916b622 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/Legacy/CSharpCodeParser.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/Legacy/CSharpCodeParser.cs @@ -1820,6 +1820,9 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy Span.ChunkGenerator = new StatementChunkGenerator(); var existingEditHandler = Span.EditHandler; Span.EditHandler = new CodeBlockEditHandler(Language.TokenizeString); + + AddMarkerSymbolIfNecessary(); + Output(SpanKindInternal.Code); Span.EditHandler = existingEditHandler; diff --git a/src/Microsoft.VisualStudio.Editor.Razor/BraceSmartIndenter.cs b/src/Microsoft.VisualStudio.Editor.Razor/BraceSmartIndenter.cs index 48da2b332e..2963fc9b0f 100644 --- a/src/Microsoft.VisualStudio.Editor.Razor/BraceSmartIndenter.cs +++ b/src/Microsoft.VisualStudio.Editor.Razor/BraceSmartIndenter.cs @@ -2,7 +2,6 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using System.Diagnostics; using System.Text; using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.Language.Legacy; @@ -11,6 +10,7 @@ using Microsoft.VisualStudio.Text; using Microsoft.VisualStudio.Text.Editor; using Microsoft.VisualStudio.Text.Operations; using ITextBuffer = Microsoft.VisualStudio.Text.ITextBuffer; +using Span = Microsoft.AspNetCore.Razor.Language.Legacy.Span; namespace Microsoft.VisualStudio.Editor.Razor { @@ -210,7 +210,7 @@ namespace Microsoft.VisualStudio.Editor.Razor var focusedTextView = documentTracker.GetFocusedTextView(); if (focusedTextView != null && ParserHelpers.IsNewLine(finalText)) { - if (!AtValidContentKind(changePosition, syntaxTree)) + if (!AtApplicableRazorBlock(changePosition, syntaxTree)) { context = null; return false; @@ -242,35 +242,68 @@ namespace Microsoft.VisualStudio.Editor.Razor } // Internal for testing - internal static bool AtValidContentKind(int changePosition, RazorSyntaxTree syntaxTree) + internal static bool AtApplicableRazorBlock(int changePosition, RazorSyntaxTree syntaxTree) { + // Our goal here is to return true when we're acting on code blocks that have all + // whitespace content and are surrounded by metacode. + // Some examples: + // @functions { |} + // @section foo { |} + // @{ |} + var change = new SourceChange(changePosition, 0, string.Empty); var owner = syntaxTree.Root.LocateOwner(change); - if (owner == null) + if (IsUnlinkedSpan(owner)) { return false; } - if (owner.Kind == SpanKindInternal.MetaCode) + if (SurroundedByInvalidContent(owner)) { - // @functions{|} - return true; + return false; } - if (owner.Kind == SpanKindInternal.Code) + if (ContainsInvalidContent(owner)) { - // It's important that we still indent in C# cases because in the example below we're asked for - // a content validation kind check at a 0 length C# code Span (marker). - // In the case that we do a smart indent in a situation when Roslyn would: Roslyn respects our - // indentation attempt and applies any additional modifications to the applicable span. - // @{|} - return true; + return false; } - + + // Indentable content inside of a code block. + return true; + } + + // Internal for testing + internal static bool ContainsInvalidContent(Span owner) + { + // We only support whitespace based content. Any non-whitespace content is an unkonwn to us + // in regards to indentation. + for (var i = 0; i < owner.Symbols.Count; i++) + { + if (!string.IsNullOrWhiteSpace(owner.Symbols[i].Content)) + { + return true; + } + } + return false; } + // Internal for testing + internal static bool IsUnlinkedSpan(Span owner) + { + return owner == null || + owner.Next == null || + owner.Previous == null; + } + + // Internal for testing + internal static bool SurroundedByInvalidContent(Span owner) + { + return owner.Next.Kind != SpanKindInternal.MetaCode || + owner.Previous.Kind != SpanKindInternal.MetaCode; + } + internal static bool BeforeClosingBrace(int linePosition, ITextSnapshotLine lineSnapshot) { var lineText = lineSnapshot.GetText(); diff --git a/test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/CSharpAutoCompleteTest.cs b/test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/CSharpAutoCompleteTest.cs index fba29ab569..0ea7e057cc 100644 --- a/test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/CSharpAutoCompleteTest.cs +++ b/test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/CSharpAutoCompleteTest.cs @@ -26,7 +26,8 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy new DirectiveBlock(chunkGenerator, Factory.CodeTransition(), Factory.MetaCode("functions").Accepts(AcceptedCharactersInternal.None), - Factory.MetaCode("{").AutoCompleteWith("}", atEndOfSpan: true).Accepts(AcceptedCharactersInternal.None))); + Factory.MetaCode("{").AutoCompleteWith("}", atEndOfSpan: true).Accepts(AcceptedCharactersInternal.None), + Factory.EmptyCSharp().AsCodeBlock())); } [Fact] diff --git a/test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/CSharpDirectivesTest.cs b/test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/CSharpDirectivesTest.cs index f8a1359575..c5a9fd9ed1 100644 --- a/test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/CSharpDirectivesTest.cs +++ b/test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/CSharpDirectivesTest.cs @@ -949,7 +949,8 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy Factory.Span(SpanKindInternal.Markup, " ", markup: false).Accepts(AcceptedCharactersInternal.AllWhiteSpace), Factory.MetaCode("{") .AutoCompleteWith("}", atEndOfSpan: true) - .Accepts(AcceptedCharactersInternal.None))); + .Accepts(AcceptedCharactersInternal.None), + Factory.EmptyCSharp().AsCodeBlock())); } [Fact] diff --git a/test/Microsoft.AspNetCore.Razor.Language.Test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/IncompleteDirectives_DesignTime.codegen.cs b/test/Microsoft.AspNetCore.Razor.Language.Test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/IncompleteDirectives_DesignTime.codegen.cs index 03f5c03321..9e76084ccd 100644 --- a/test/Microsoft.AspNetCore.Razor.Language.Test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/IncompleteDirectives_DesignTime.codegen.cs +++ b/test/Microsoft.AspNetCore.Razor.Language.Test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/IncompleteDirectives_DesignTime.codegen.cs @@ -63,6 +63,11 @@ global::System.Object __typeHelper = "; #pragma warning disable 1998 public async System.Threading.Tasks.Task ExecuteAsync() { +#line 25 "TestFiles/IntegrationTests/CodeGenerationIntegrationTest/IncompleteDirectives.cshtml" + + +#line default +#line hidden } #pragma warning restore 1998 } diff --git a/test/Microsoft.AspNetCore.Razor.Language.Test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/IncompleteDirectives_DesignTime.ir.txt b/test/Microsoft.AspNetCore.Razor.Language.Test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/IncompleteDirectives_DesignTime.ir.txt index f21273aeb2..ba9fae429d 100644 --- a/test/Microsoft.AspNetCore.Razor.Language.Test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/IncompleteDirectives_DesignTime.ir.txt +++ b/test/Microsoft.AspNetCore.Razor.Language.Test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/IncompleteDirectives_DesignTime.ir.txt @@ -82,3 +82,5 @@ Document - HtmlContent - (339:23,9 [3] IncompleteDirectives.cshtml) IntermediateToken - (339:23,9 [3] IncompleteDirectives.cshtml) - Html - {\n MalformedDirective - (342:24,0 [12] IncompleteDirectives.cshtml) - functions + CSharpCode - (354:24,12 [0] IncompleteDirectives.cshtml) + IntermediateToken - (354:24,12 [0] IncompleteDirectives.cshtml) - CSharp - diff --git a/test/Microsoft.AspNetCore.Razor.Language.Test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/IncompleteDirectives_DesignTime.mappings.txt b/test/Microsoft.AspNetCore.Razor.Language.Test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/IncompleteDirectives_DesignTime.mappings.txt index b5f9ac8df5..41510ddf4d 100644 --- a/test/Microsoft.AspNetCore.Razor.Language.Test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/IncompleteDirectives_DesignTime.mappings.txt +++ b/test/Microsoft.AspNetCore.Razor.Language.Test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/IncompleteDirectives_DesignTime.mappings.txt @@ -63,3 +63,8 @@ Source Location: (326:21,9 [0] TestFiles/IntegrationTests/CodeGenerationIntegrat Generated Location: (1463:55,0 [0] ) || +Source Location: (354:24,12 [0] TestFiles/IntegrationTests/CodeGenerationIntegrationTest/IncompleteDirectives.cshtml) +|| +Generated Location: (1879:66,12 [0] ) +|| + diff --git a/test/Microsoft.AspNetCore.Razor.Language.Test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/IncompleteDirectives_Runtime.ir.txt b/test/Microsoft.AspNetCore.Razor.Language.Test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/IncompleteDirectives_Runtime.ir.txt index ecffead8c5..5a369ff69c 100644 --- a/test/Microsoft.AspNetCore.Razor.Language.Test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/IncompleteDirectives_Runtime.ir.txt +++ b/test/Microsoft.AspNetCore.Razor.Language.Test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/IncompleteDirectives_Runtime.ir.txt @@ -52,3 +52,5 @@ Document - HtmlContent - (339:23,9 [3] IncompleteDirectives.cshtml) IntermediateToken - (339:23,9 [3] IncompleteDirectives.cshtml) - Html - {\n MalformedDirective - (342:24,0 [12] IncompleteDirectives.cshtml) - functions + CSharpCode - (354:24,12 [0] IncompleteDirectives.cshtml) + IntermediateToken - (354:24,12 [0] IncompleteDirectives.cshtml) - CSharp - diff --git a/test/Microsoft.VisualStudio.Editor.Razor.Test/BraceSmartIndenterTest.cs b/test/Microsoft.VisualStudio.Editor.Razor.Test/BraceSmartIndenterTest.cs index a5041c083e..270165997d 100644 --- a/test/Microsoft.VisualStudio.Editor.Razor.Test/BraceSmartIndenterTest.cs +++ b/test/Microsoft.VisualStudio.Editor.Razor.Test/BraceSmartIndenterTest.cs @@ -5,47 +5,232 @@ using System; using System.Collections.Generic; using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.Language.Extensions; +using Microsoft.AspNetCore.Razor.Language.Legacy; using Microsoft.VisualStudio.Test; using Microsoft.VisualStudio.Text; using Microsoft.VisualStudio.Text.Editor; using Microsoft.VisualStudio.Text.Operations; using Moq; using Xunit; +using ITextBuffer = Microsoft.VisualStudio.Text.ITextBuffer; +using Span = Microsoft.AspNetCore.Razor.Language.Legacy.Span; namespace Microsoft.VisualStudio.Editor.Razor { public class BraceSmartIndenterTest : BraceSmartIndenterTestBase { [Fact] - public void AtValidContentKind_ReturnsFalseAtMarkup() + public void AtApplicableRazorBlock_NestedIfBlock_ReturnsFalse() { // Arrange - var syntaxTree = RazorSyntaxTree.Parse(TestRazorSourceDocument.Create("
")); - var changePosition = 2; + var syntaxTree = GetSyntaxTree("@{ if (true) { } }"); // Act - var result = BraceSmartIndenter.AtValidContentKind(changePosition, syntaxTree); + var result = BraceSmartIndenter.AtApplicableRazorBlock(14, syntaxTree); // Assert Assert.False(result); } [Fact] - public void AtValidContentKind_ReturnsTrueAtCode() + public void AtApplicableRazorBlock_SectionBlock_ReturnsTrue() { // Arrange - var syntaxTree = RazorSyntaxTree.Parse(TestRazorSourceDocument.Create("@{}")); - var changePosition = 2; + var syntaxTree = GetSyntaxTree("@section Foo { }"); // Act - var result = BraceSmartIndenter.AtValidContentKind(changePosition, syntaxTree); + var result = BraceSmartIndenter.AtApplicableRazorBlock(15, syntaxTree); // Assert Assert.True(result); } [Fact] - public void AtValidContentKind_ReturnsTrueAtMetacode() + public void AtApplicableRazorBlock_FunctionsBlock_ReturnsTrue() + { + // Arrange + var syntaxTree = GetSyntaxTree("@functions { }"); + + // Act + var result = BraceSmartIndenter.AtApplicableRazorBlock(13, syntaxTree); + + // Assert + Assert.True(result); + } + + [Fact] + public void AtApplicableRazorBlock_ExplicitCodeBlock_ReturnsTrue() + { + // Arrange + var syntaxTree = GetSyntaxTree("@{ }"); + + // Act + var result = BraceSmartIndenter.AtApplicableRazorBlock(3, syntaxTree); + + // Assert + Assert.True(result); + } + + [Fact] + public void ContainsInvalidContent_NewLineSpan_ReturnsFalse() + { + // Arrange + var span = ExtractSpan(2, "@{" + Environment.NewLine + "}"); + + // Act + var result = BraceSmartIndenter.ContainsInvalidContent(span); + + // Assert + Assert.False(result); + } + + [Fact] + public void ContainsInvalidContent_WhitespaceSpan_ReturnsFalse() + { + // Arrange + var span = ExtractSpan(2, "@{ }"); + + // Act + var result = BraceSmartIndenter.ContainsInvalidContent(span); + + // Assert + Assert.False(result); + } + + [Fact] + public void ContainsInvalidContent_MarkerSpan_ReturnsFalse() + { + // Arrange + var span = ExtractSpan(3, "@{}"); + + // Act + var result = BraceSmartIndenter.ContainsInvalidContent(span); + + // Assert + Assert.False(result); + } + + [Fact] + public void ContainsInvalidContent_NonWhitespaceMarker_ReturnsTrue() + { + // Arrange + var span = ExtractSpan(2, "@{ if}"); + + // Act + var result = BraceSmartIndenter.ContainsInvalidContent(span); + + // Assert + Assert.True(result); + } + + [Fact] + public void IsUnlinkedSpan_NullPrevious_ReturnsTrue() + { + // Arrange + var span = ExtractSpan(0, "@{}"); + + // Act + var result = BraceSmartIndenter.IsUnlinkedSpan(span); + + // Assert + Assert.True(result); + } + + [Fact] + public void IsUnlinkedSpan_NullNext_ReturnsTrue() + { + // Arrange + var span = ExtractSpan(3, "@{}"); + + // Act + var result = BraceSmartIndenter.IsUnlinkedSpan(span); + + // Assert + Assert.True(result); + } + + [Fact] + public void IsUnlinkedSpan_NullOwner_ReturnsTrue() + { + // Arrange + Span owner = null; + + // Act + var result = BraceSmartIndenter.IsUnlinkedSpan(owner); + + // Assert + Assert.True(result); + } + + [Fact] + public void SurroundedByInvalidContent_MetacodeSurroundings_ReturnsFalse() + { + // Arrange + var span = ExtractSpan(2, "@{}"); + + // Act + var result = BraceSmartIndenter.SurroundedByInvalidContent(span); + + // Assert + Assert.False(result); + } + + [Fact] + public void SurroundedByInvalidContent_OnlyNextMetacode_ReturnsTrue() + { + // Arrange + var span = ExtractSpan(9, "@{}"); + + // Act + var result = BraceSmartIndenter.SurroundedByInvalidContent(span); + + // Assert + Assert.True(result); + } + + [Fact] + public void SurroundedByInvalidContent_OnlyPreviousMetacode_ReturnsTrue() + { + // Arrange + var span = ExtractSpan(2, "@{"); + + // Act + var result = BraceSmartIndenter.SurroundedByInvalidContent(span); + + // Assert + Assert.True(result); + } + + [Fact] + public void AtApplicableRazorBlock_AtMarkup_ReturnsFalse() + { + // Arrange + var syntaxTree = RazorSyntaxTree.Parse(TestRazorSourceDocument.Create("
")); + var changePosition = 2; + + // Act + var result = BraceSmartIndenter.AtApplicableRazorBlock(changePosition, syntaxTree); + + // Assert + Assert.False(result); + } + + [Fact] + public void AtApplicableRazorBlock_AtExplicitCodeBlocksCode_ReturnsTrue() + { + // Arrange + var syntaxTree = RazorSyntaxTree.Parse(TestRazorSourceDocument.Create("@{}")); + var changePosition = 2; + + // Act + var result = BraceSmartIndenter.AtApplicableRazorBlock(changePosition, syntaxTree); + + // Assert + Assert.True(result); + } + + [Fact] + public void AtApplicableRazorBlock_AtMetacode_ReturnsTrue() { // Arrange var parseOptions = RazorParserOptions.Create(options => options.Directives.Add(FunctionsDirective.Directive)); @@ -53,21 +238,21 @@ namespace Microsoft.VisualStudio.Editor.Razor var changePosition = 12; // Act - var result = BraceSmartIndenter.AtValidContentKind(changePosition, syntaxTree); + var result = BraceSmartIndenter.AtApplicableRazorBlock(changePosition, syntaxTree); // Assert Assert.True(result); } [Fact] - public void AtValidContentKind_ReturnsFalseWhenNoOwner() + public void AtApplicableRazorBlock_WhenNoOwner_ReturnsFalse() { // Arrange var syntaxTree = RazorSyntaxTree.Parse(TestRazorSourceDocument.Create("@DateTime.Now")); var changePosition = 14; // 1 after the end of the content // Act - var result = BraceSmartIndenter.AtValidContentKind(changePosition, syntaxTree); + var result = BraceSmartIndenter.AtApplicableRazorBlock(changePosition, syntaxTree); // Assert Assert.False(result); @@ -339,6 +524,27 @@ namespace Microsoft.VisualStudio.Editor.Razor Assert.True(result); } + private static RazorSyntaxTree GetSyntaxTree(string content) + { + var syntaxTree = RazorSyntaxTree.Parse( + TestRazorSourceDocument.Create(content), + RazorParserOptions.Create(options => + { + options.Directives.Add(FunctionsDirective.Directive); + options.Directives.Add(SectionDirective.Directive); + })); + syntaxTree.Root.LinkNodes(); + + return syntaxTree; + } + + private static Span ExtractSpan(int spanLocation, string content) + { + var syntaxTree = GetSyntaxTree(content); + var span = syntaxTree.Root.LocateOwner(new SourceChange(new SourceSpan(spanLocation, 0), string.Empty)); + return span; + } + protected class TestTextContentChangedEventArgs : TextContentChangedEventArgs { public TestTextContentChangedEventArgs(INormalizedTextChangeCollection changeCollection)