diff --git a/src/Microsoft.AspNetCore.Razor.Language/Legacy/Block.cs b/src/Microsoft.AspNetCore.Razor.Language/Legacy/Block.cs index 781a6e0b5b..4c8f962281 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/Legacy/Block.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/Legacy/Block.cs @@ -214,6 +214,20 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy visitor.VisitBlock(this); } + public override SyntaxTreeNode Clone() + { + var blockBuilder = new BlockBuilder(this); + + blockBuilder.Children.Clear(); + for (var i = 0; i < Children.Count; i++) + { + var clonedChild = Children[i].Clone(); + blockBuilder.Children.Add(clonedChild); + } + + return blockBuilder.Build(); + } + private class EquivalenceComparer : IEqualityComparer { public static readonly EquivalenceComparer Default = new EquivalenceComparer(); diff --git a/src/Microsoft.AspNetCore.Razor.Language/Legacy/Span.cs b/src/Microsoft.AspNetCore.Razor.Language/Legacy/Span.cs index 2e7a04189c..7bbd0b1cab 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/Legacy/Span.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/Legacy/Span.cs @@ -151,5 +151,11 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy { visitor.VisitSpan(this); } + + public override SyntaxTreeNode Clone() + { + var spanBuilder = new SpanBuilder(this); + return spanBuilder.Build(); + } } } diff --git a/src/Microsoft.AspNetCore.Razor.Language/Legacy/SyntaxTreeNode.cs b/src/Microsoft.AspNetCore.Razor.Language/Legacy/SyntaxTreeNode.cs index ed5ef74181..a20c7c318b 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/Legacy/SyntaxTreeNode.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/Legacy/SyntaxTreeNode.cs @@ -43,5 +43,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy public abstract int GetEquivalenceHash(); public abstract void Accept(ParserVisitor visitor); + + public abstract SyntaxTreeNode Clone(); } } diff --git a/src/Microsoft.AspNetCore.Razor.Language/Legacy/TagHelperBlock.cs b/src/Microsoft.AspNetCore.Razor.Language/Legacy/TagHelperBlock.cs index 9d3f7167be..b8863247bd 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/Legacy/TagHelperBlock.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/Legacy/TagHelperBlock.cs @@ -159,6 +159,41 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy return null; } + public override SyntaxTreeNode Clone() + { + var tagHelperBlockBuilder = new TagHelperBlockBuilder(this); + + tagHelperBlockBuilder.Children.Clear(); + for (var i = 0; i < Children.Count; i++) + { + var clonedChild = Children[i].Clone(); + tagHelperBlockBuilder.Children.Add(clonedChild); + } + + tagHelperBlockBuilder.Attributes.Clear(); + for (var i = 0; i < Attributes.Count; i++) + { + var existingAttribute = Attributes[i]; + var clonedValue = existingAttribute.Value != null ? existingAttribute.Value.Clone() : null; + tagHelperBlockBuilder.Attributes.Add( + new TagHelperAttributeNode(existingAttribute.Name, clonedValue, existingAttribute.AttributeStructure)); + } + + if (SourceStartTag != null) + { + var clonedStartTag = (Block)SourceStartTag.Clone(); + tagHelperBlockBuilder.SourceStartTag = clonedStartTag; + } + + if (SourceEndTag != null) + { + var clonedEndTag = (Block)SourceEndTag.Clone(); + tagHelperBlockBuilder.SourceEndTag = clonedEndTag; + } + + return tagHelperBlockBuilder.Build(); + } + /// public override string ToString() { diff --git a/src/Microsoft.AspNetCore.Razor.Language/Legacy/TagHelperBlockBuilder.cs b/src/Microsoft.AspNetCore.Razor.Language/Legacy/TagHelperBlockBuilder.cs index b05edd8941..29e39bb3ae 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/Legacy/TagHelperBlockBuilder.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/Legacy/TagHelperBlockBuilder.cs @@ -18,9 +18,12 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy public TagHelperBlockBuilder(TagHelperBlock original) : base(original) { - TagName = original.TagName; + SourceStartTag = original.SourceStartTag; + SourceEndTag = original.SourceEndTag; + TagMode = original.TagMode; BindingResult = original.Binding; Attributes = new List(original.Attributes); + TagName = original.TagName; } /// diff --git a/src/Microsoft.VisualStudio.Editor.Razor/DefaultVisualStudioRazorParser.cs b/src/Microsoft.VisualStudio.Editor.Razor/DefaultVisualStudioRazorParser.cs index baa9aa049c..053b980e86 100644 --- a/src/Microsoft.VisualStudio.Editor.Razor/DefaultVisualStudioRazorParser.cs +++ b/src/Microsoft.VisualStudio.Editor.Razor/DefaultVisualStudioRazorParser.cs @@ -25,6 +25,7 @@ namespace Microsoft.VisualStudio.Editor.Razor internal Timer _idleTimer; internal BackgroundParser _parser; internal ChangeReference _latestChangeReference; + internal RazorSyntaxTreePartialParser _partialParser; private readonly object IdleLock = new object(); private readonly VisualStudioCompletionBroker _completionBroker; @@ -32,7 +33,6 @@ namespace Microsoft.VisualStudio.Editor.Razor private readonly ForegroundDispatcher _dispatcher; private readonly RazorTemplateEngineFactoryService _templateEngineFactory; private readonly ErrorReporter _errorReporter; - private RazorSyntaxTreePartialParser _partialParser; private RazorTemplateEngine _templateEngine; private RazorCodeDocument _codeDocument; private ITextSnapshot _snapshot; diff --git a/src/Microsoft.VisualStudio.Editor.Razor/RazorSyntaxTreePartialParser.cs b/src/Microsoft.VisualStudio.Editor.Razor/RazorSyntaxTreePartialParser.cs index 9bd297d3e4..377351c959 100644 --- a/src/Microsoft.VisualStudio.Editor.Razor/RazorSyntaxTreePartialParser.cs +++ b/src/Microsoft.VisualStudio.Editor.Razor/RazorSyntaxTreePartialParser.cs @@ -1,6 +1,7 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System; using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.Language.Legacy; using Span = Microsoft.AspNetCore.Razor.Language.Legacy.Span; @@ -9,15 +10,24 @@ namespace Microsoft.VisualStudio.Editor.Razor { internal class RazorSyntaxTreePartialParser { - private readonly RazorSyntaxTree _syntaxTree; private Span _lastChangeOwner; private bool _lastResultProvisional; public RazorSyntaxTreePartialParser(RazorSyntaxTree syntaxTree) { - _syntaxTree = syntaxTree; + if (syntaxTree == null) + { + throw new ArgumentNullException(nameof(syntaxTree)); + } + + // We mutate the existing syntax tree so we need to clone the one passed in so our mutations don't + // impact external state. + SyntaxTreeRoot = (Block)syntaxTree.Root.Clone(); } + // Internal for testing + internal Block SyntaxTreeRoot { get; } + public PartialParseResultInternal Parse(SourceChange change) { var result = GetPartialParseResult(change); @@ -46,7 +56,7 @@ namespace Microsoft.VisualStudio.Editor.Razor } // Locate the span responsible for this change - _lastChangeOwner = _syntaxTree.Root.LocateOwner(change); + _lastChangeOwner = SyntaxTreeRoot.LocateOwner(change); if (_lastResultProvisional) { diff --git a/test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/BlockTest.cs b/test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/BlockTest.cs index aa826bb323..264d18c424 100644 --- a/test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/BlockTest.cs +++ b/test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/BlockTest.cs @@ -8,6 +8,26 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy { public class BlockTest { + [Fact] + public void Clone_ClonesBlock() + { + // Arrange + var blockBuilder = new BlockBuilder() + { + ChunkGenerator = new DynamicAttributeBlockChunkGenerator(new LocationTagged("class=\"", SourceLocation.Zero), 0, 0, 0), + Type = BlockKindInternal.Expression, + }; + blockBuilder.Children.Add(new SpanBuilder(new SourceLocation(1, 2, 3)).Build()); + var block = blockBuilder.Build(); + + // Act + var copy = (Block)block.Clone(); + + // Assert + ParserTestBase.EvaluateParseTree(copy, block); + Assert.NotSame(block, copy); + } + [Fact] public void ConstructorWithBlockBuilderSetsParent() { diff --git a/test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/SpanTest.cs b/test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/SpanTest.cs new file mode 100644 index 0000000000..2cb103d389 --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/SpanTest.cs @@ -0,0 +1,31 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Xunit; + +namespace Microsoft.AspNetCore.Razor.Language.Legacy +{ + public class SpanTest + { + [Fact] + public void Clone_ClonesSpan() + { + // Arrange + var spanBuilder = new SpanBuilder(new SourceLocation(1, 2, 3)) + { + EditHandler = new SpanEditHandler(CSharpLanguageCharacteristics.Instance.TokenizeString), + Kind = SpanKindInternal.Transition, + ChunkGenerator = new ExpressionChunkGenerator(), + }; + spanBuilder.Accept(new CSharpSymbol("@", CSharpSymbolType.Transition)); + var span = spanBuilder.Build(); + + // Act + var copy = (Span)span.Clone(); + + // Assert + Assert.Equal(span, copy); + Assert.NotSame(span, copy); + } + } +} diff --git a/test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/TagHelperBlockTest.cs b/test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/TagHelperBlockTest.cs index e43a3ecf78..971e97f361 100644 --- a/test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/TagHelperBlockTest.cs +++ b/test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/TagHelperBlockTest.cs @@ -1,12 +1,114 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System.Collections.Generic; +using System.Linq; using Xunit; namespace Microsoft.AspNetCore.Razor.Language.Legacy { public class TagHelperBlockTest { + [Fact] + public void Clone_ClonesTagHelperChildren() + { + // Arrange + var tagHelper = new TagHelperBlockBuilder( + "p", + TagMode.StartTagAndEndTag, + attributes: new List(), + children: new[] + { + new SpanBuilder(SourceLocation.Zero).Build(), + new SpanBuilder(new SourceLocation(0, 1, 2)).Build(), + }).Build(); + + // Act + var copy = (TagHelperBlock)tagHelper.Clone(); + + // Assert + ParserTestBase.EvaluateParseTree(copy, tagHelper); + Assert.Collection( + copy.Children, + child => Assert.NotSame(tagHelper.Children[0], child), + child => Assert.NotSame(tagHelper.Children[1], child)); + } + + [Fact] + public void Clone_ClonesTagHelperAttributes() + { + // Arrange + var tagHelper = (TagHelperBlock)new TagHelperBlockBuilder( + "p", + TagMode.StartTagAndEndTag, + attributes: new List() + { + new TagHelperAttributeNode("class", new SpanBuilder(SourceLocation.Zero).Build(), AttributeStructure.NoQuotes), + new TagHelperAttributeNode("checked", new SpanBuilder(SourceLocation.Undefined).Build(), AttributeStructure.NoQuotes) + }, + children: Enumerable.Empty()).Build(); + + // Act + var copy = (TagHelperBlock)tagHelper.Clone(); + + // Assert + ParserTestBase.EvaluateParseTree(copy, tagHelper); + Assert.Collection( + copy.Attributes, + attribute => Assert.NotSame(tagHelper.Attributes[0], attribute), + attribute => Assert.NotSame(tagHelper.Attributes[1], attribute)); + } + + [Fact] + public void Clone_ClonesTagHelperSourceStartTag() + { + // Arrange + var tagHelper = (TagHelperBlock)new TagHelperBlockBuilder( + "p", + TagMode.StartTagAndEndTag, + attributes: new List(), + children: Enumerable.Empty()) + { + SourceStartTag = new BlockBuilder() + { + Type = BlockKindInternal.Comment, + ChunkGenerator = new RazorCommentChunkGenerator() + }.Build() + }.Build(); + + // Act + var copy = (TagHelperBlock)tagHelper.Clone(); + + // Assert + ParserTestBase.EvaluateParseTree(copy, tagHelper); + Assert.NotSame(tagHelper.SourceStartTag, copy.SourceStartTag); + } + + [Fact] + public void Clone_ClonesTagHelperSourceEndTag() + { + // Arrange + var tagHelper = (TagHelperBlock)new TagHelperBlockBuilder( + "p", + TagMode.StartTagAndEndTag, + attributes: new List(), + children: Enumerable.Empty()) + { + SourceEndTag = new BlockBuilder() + { + Type = BlockKindInternal.Comment, + ChunkGenerator = new RazorCommentChunkGenerator() + }.Build() + }.Build(); + + // Act + var copy = (TagHelperBlock)tagHelper.Clone(); + + // Assert + ParserTestBase.EvaluateParseTree(copy, tagHelper); + Assert.NotSame(tagHelper.SourceEndTag, copy.SourceEndTag); + } + [Fact] public void FlattenFlattensSelfClosingTagHelpers() { diff --git a/test/Microsoft.VisualStudio.Editor.Razor.Test/DefaultVisualStudioRazorParserIntegrationTest.cs b/test/Microsoft.VisualStudio.Editor.Razor.Test/DefaultVisualStudioRazorParserIntegrationTest.cs index 6943801c87..cb7df55d12 100644 --- a/test/Microsoft.VisualStudio.Editor.Razor.Test/DefaultVisualStudioRazorParserIntegrationTest.cs +++ b/test/Microsoft.VisualStudio.Editor.Razor.Test/DefaultVisualStudioRazorParserIntegrationTest.cs @@ -97,7 +97,7 @@ namespace Microsoft.VisualStudio.Editor.Razor { manager.ApplyEdit(testEdit); Assert.Equal(1, manager.ParseCount); - ParserTestBase.EvaluateParseTree(manager.CurrentSyntaxTree.Root, new MarkupBlock( + ParserTestBase.EvaluateParseTree(manager.PartialParsingSyntaxTreeRoot, new MarkupBlock( factory.EmptyHtml(), new StatementBlock( factory.CodeTransition(), @@ -156,7 +156,7 @@ namespace Microsoft.VisualStudio.Editor.Razor { manager.ApplyEdit(testEdit); Assert.Equal(1, manager.ParseCount); - ParserTestBase.EvaluateParseTree(manager.CurrentSyntaxTree.Root, new MarkupBlock( + ParserTestBase.EvaluateParseTree(manager.PartialParsingSyntaxTreeRoot, new MarkupBlock( factory.EmptyHtml(), new StatementBlock( factory.CodeTransition(), @@ -203,7 +203,7 @@ namespace Microsoft.VisualStudio.Editor.Razor manager.ApplyEdit(testEdit); Assert.Equal(1, manager.ParseCount); - ParserTestBase.EvaluateParseTree(manager.CurrentSyntaxTree.Root, new MarkupBlock( + ParserTestBase.EvaluateParseTree(manager.PartialParsingSyntaxTreeRoot, new MarkupBlock( factory.Markup("foo "), new ExpressionBlock( factory.CodeTransition(), @@ -249,7 +249,7 @@ namespace Microsoft.VisualStudio.Editor.Razor manager.ApplyEdit(testEdit); Assert.Equal(1, manager.ParseCount); - ParserTestBase.EvaluateParseTree(manager.CurrentSyntaxTree.Root, new MarkupBlock( + ParserTestBase.EvaluateParseTree(manager.PartialParsingSyntaxTreeRoot, new MarkupBlock( factory.Markup("foo "), new ExpressionBlock( factory.CodeTransition(), @@ -301,7 +301,7 @@ namespace Microsoft.VisualStudio.Editor.Razor applyEdit(); Assert.Equal(1, manager.ParseCount); - ParserTestBase.EvaluateParseTree(manager.CurrentSyntaxTree.Root, new MarkupBlock( + ParserTestBase.EvaluateParseTree(manager.PartialParsingSyntaxTreeRoot, new MarkupBlock( factory.Markup("foo "), new ExpressionBlock( factory.CodeTransition(), @@ -369,7 +369,7 @@ namespace Microsoft.VisualStudio.Editor.Razor // Assert Assert.Equal(2, manager.ParseCount); - ParserTestBase.EvaluateParseTree(manager.CurrentSyntaxTree.Root, + ParserTestBase.EvaluateParseTree(manager.PartialParsingSyntaxTreeRoot, new MarkupBlock( factory.Markup("foo "), new ExpressionBlock( @@ -406,7 +406,7 @@ namespace Microsoft.VisualStudio.Editor.Razor // Assert Assert.Equal(1, manager.ParseCount); - ParserTestBase.EvaluateParseTree(manager.CurrentSyntaxTree.Root, + ParserTestBase.EvaluateParseTree(manager.PartialParsingSyntaxTreeRoot, new MarkupBlock( factory.Markup("foo "), new ExpressionBlock( @@ -642,6 +642,8 @@ namespace Microsoft.VisualStudio.Editor.Razor public RazorSyntaxTree CurrentSyntaxTree { get; private set; } + public Block PartialParsingSyntaxTreeRoot => _parser._partialParser.SyntaxTreeRoot; + public async Task InitializeWithDocumentAsync(ITextSnapshot snapshot) { var old = new StringTextSnapshot(string.Empty); diff --git a/test/Microsoft.VisualStudio.Editor.Razor.Test/DefaultVisualStudioRazorParserTest.cs b/test/Microsoft.VisualStudio.Editor.Razor.Test/DefaultVisualStudioRazorParserTest.cs index 494e9ed190..38075b0460 100644 --- a/test/Microsoft.VisualStudio.Editor.Razor.Test/DefaultVisualStudioRazorParserTest.cs +++ b/test/Microsoft.VisualStudio.Editor.Razor.Test/DefaultVisualStudioRazorParserTest.cs @@ -117,10 +117,12 @@ namespace Microsoft.VisualStudio.Editor.Razor var latestChange = new SourceChange(0, 0, string.Empty); var latestSnapshot = documentTracker.TextBuffer.CurrentSnapshot; parser._latestChangeReference = new DefaultVisualStudioRazorParser.ChangeReference(latestChange, latestSnapshot); + var codeDocument = TestRazorCodeDocument.CreateEmpty(); + codeDocument.SetSyntaxTree(RazorSyntaxTree.Parse(TestRazorSourceDocument.Create())); var args = new DocumentStructureChangedEventArgs( latestChange, latestSnapshot, - TestRazorCodeDocument.CreateEmpty()); + codeDocument); // Act parser.OnDocumentStructureChanged(args); diff --git a/test/Microsoft.VisualStudio.Editor.Razor.Test/RazorSyntaxTreePartialParserTest.cs b/test/Microsoft.VisualStudio.Editor.Razor.Test/RazorSyntaxTreePartialParserTest.cs index 544f74a91d..b82bf58c6b 100644 --- a/test/Microsoft.VisualStudio.Editor.Razor.Test/RazorSyntaxTreePartialParserTest.cs +++ b/test/Microsoft.VisualStudio.Editor.Razor.Test/RazorSyntaxTreePartialParserTest.cs @@ -3,12 +3,14 @@ using System; using System.Collections.Generic; +using System.Linq; using Microsoft.AspNetCore.Mvc.Razor.Extensions; using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.Language.Legacy; using Microsoft.VisualStudio.Test; using Microsoft.VisualStudio.Text; using Xunit; +using Span = Microsoft.AspNetCore.Razor.Language.Legacy.Span; namespace Microsoft.VisualStudio.Editor.Razor { @@ -566,7 +568,7 @@ namespace Microsoft.VisualStudio.Editor.Razor var result = parser.Parse(edit.Change); Assert.Equal(PartialParseResultInternal.Accepted | additionalFlags, result); - ParserTestBase.EvaluateParseTree(expectedTree, syntaxTree.Root); + ParserTestBase.EvaluateParseTree(parser.SyntaxTreeRoot, expectedTree); } private static TestEdit CreateInsertionChange(string initialText, int insertionLocation, string insertionText)