diff --git a/src/Microsoft.AspNetCore.Razor/Parser/SyntaxTree/Block.cs b/src/Microsoft.AspNetCore.Razor/Parser/SyntaxTree/Block.cs index 4b282627d4..85baee80e1 100644 --- a/src/Microsoft.AspNetCore.Razor/Parser/SyntaxTree/Block.cs +++ b/src/Microsoft.AspNetCore.Razor/Parser/SyntaxTree/Block.cs @@ -154,11 +154,13 @@ namespace Microsoft.AspNetCore.Razor.Parser.SyntaxTree } } - public Span LocateOwner(TextChange change) + public virtual Span LocateOwner(TextChange change) => LocateOwner(change, Children); + + protected static Span LocateOwner(TextChange change, IEnumerable elements) { // Ask each child recursively Span owner = null; - foreach (SyntaxTreeNode element in Children) + foreach (var element in elements) { var span = element as Span; if (span == null) diff --git a/src/Microsoft.AspNetCore.Razor/Parser/TagHelpers/Internal/TagHelperBlockRewriter.cs b/src/Microsoft.AspNetCore.Razor/Parser/TagHelpers/Internal/TagHelperBlockRewriter.cs index 4f2f4129fc..2a786f8460 100644 --- a/src/Microsoft.AspNetCore.Razor/Parser/TagHelpers/Internal/TagHelperBlockRewriter.cs +++ b/src/Microsoft.AspNetCore.Razor/Parser/TagHelpers/Internal/TagHelperBlockRewriter.cs @@ -7,6 +7,7 @@ using System.Diagnostics; using System.Linq; using Microsoft.AspNetCore.Razor.Chunks.Generators; using Microsoft.AspNetCore.Razor.Compilation.TagHelpers; +using Microsoft.AspNetCore.Razor.Editor; using Microsoft.AspNetCore.Razor.Parser.Internal; using Microsoft.AspNetCore.Razor.Parser.SyntaxTree; using Microsoft.AspNetCore.Razor.TagHelpers; @@ -468,7 +469,8 @@ namespace Microsoft.AspNetCore.Razor.Parser.TagHelpers.Internal spanBuilder.ChunkGenerator = new MarkupChunkGenerator(); } - spanBuilder.Kind = SpanKind.Code; + ConfigureNonStringAttribute(spanBuilder); + span = spanBuilder.Build(); } } @@ -628,7 +630,7 @@ namespace Microsoft.AspNetCore.Razor.Parser.TagHelpers.Internal // SyntaxTreeNode reflects that. if (isBoundNonStringAttribute) { - builder.Kind = SpanKind.Code; + ConfigureNonStringAttribute(builder); } return builder.Build(); @@ -701,6 +703,18 @@ namespace Microsoft.AspNetCore.Razor.Parser.TagHelpers.Internal htmlSymbol.Type == HtmlSymbolType.SingleQuote; } + private static void ConfigureNonStringAttribute(SpanBuilder builder) + { + builder.Kind = SpanKind.Code; + builder.EditHandler = new ImplicitExpressionEditHandler( + builder.EditHandler.Tokenizer, + CSharpCodeParser.DefaultKeywords, + acceptTrailingDot: true) + { + AcceptedCharacters = AcceptedCharacters.AnyExceptNewline + }; + } + private class TryParseResult { public string AttributeName { get; set; } diff --git a/src/Microsoft.AspNetCore.Razor/Parser/TagHelpers/TagHelperBlock.cs b/src/Microsoft.AspNetCore.Razor/Parser/TagHelpers/TagHelperBlock.cs index b43a2fd326..f4166faea7 100644 --- a/src/Microsoft.AspNetCore.Razor/Parser/TagHelpers/TagHelperBlock.cs +++ b/src/Microsoft.AspNetCore.Razor/Parser/TagHelpers/TagHelperBlock.cs @@ -5,9 +5,10 @@ using System; using System.Collections.Generic; using System.Globalization; using System.Linq; +using Microsoft.AspNetCore.Razor.Compilation.TagHelpers; using Microsoft.AspNetCore.Razor.Parser.SyntaxTree; using Microsoft.AspNetCore.Razor.TagHelpers; -using Microsoft.AspNetCore.Razor.Compilation.TagHelpers; +using Microsoft.AspNetCore.Razor.Text; using Microsoft.Extensions.Internal; namespace Microsoft.AspNetCore.Razor.Parser.TagHelpers @@ -123,6 +124,45 @@ namespace Microsoft.AspNetCore.Razor.Parser.TagHelpers } } + public override Span LocateOwner(TextChange change) + { + var oldPosition = change.OldPosition; + if (oldPosition < Start.AbsoluteIndex) + { + // Change occurs prior to the TagHelper. + return null; + } + + var bodyEndLocation = SourceStartTag?.Start.AbsoluteIndex + SourceStartTag?.Length + base.Length; + if (oldPosition > bodyEndLocation) + { + // Change occurs after the TagHelpers body. End tags for TagHelpers cannot claim ownership of changes + // because any change to them impacts whether or not a tag is a TagHelper. + return null; + } + + var startTagEndLocation = Start.AbsoluteIndex + SourceStartTag?.Length; + if (oldPosition < startTagEndLocation) + { + // Change occurs in the start tag. + + var attributeElements = Attributes + .Select(attribute => attribute.Value) + .Where(value => value != null); + + return LocateOwner(change, attributeElements); + } + + if (oldPosition < bodyEndLocation) + { + // Change occurs in the body + return base.LocateOwner(change); + } + + // TagHelper does not contain a Span that can claim ownership. + return null; + } + /// public override string ToString() { diff --git a/test/Microsoft.AspNetCore.Razor.Test/Framework/TestSpanBuilder.cs b/test/Microsoft.AspNetCore.Razor.Test/Framework/TestSpanBuilder.cs index d030149d29..be919eff97 100644 --- a/test/Microsoft.AspNetCore.Razor.Test/Framework/TestSpanBuilder.cs +++ b/test/Microsoft.AspNetCore.Razor.Test/Framework/TestSpanBuilder.cs @@ -139,7 +139,27 @@ namespace Microsoft.AspNetCore.Razor.Test.Framework public static SpanConstructor CodeMarkup(this SpanFactory self, params string[] content) { - return self.Span(SpanKind.Code, content, markup: true).With(new MarkupChunkGenerator()); + return self + .Span(SpanKind.Code, content, markup: true) + .AsCodeMarkup(); + } + + public static SpanConstructor CSharpCodeMarkup(this SpanFactory self, string content) + { + return self.Code(content) + .AsImplicitExpression(CSharpCodeParser.DefaultKeywords, acceptTrailingDot: true) + .AsCodeMarkup(); + } + + public static SpanConstructor AsCodeMarkup(this SpanConstructor self) + { + return self + .With(new ImplicitExpressionEditHandler( + SpanConstructor.TestTokenizer, + CSharpCodeParser.DefaultKeywords, + acceptTrailingDot: true)) + .With(new MarkupChunkGenerator()) + .Accepts(AcceptedCharacters.AnyExceptNewline); } public static SourceLocation GetLocationAndAdvance(this SourceLocationTracker self, string content) diff --git a/test/Microsoft.AspNetCore.Razor.Test/Parser/TagHelpers/Internal/TagHelperBlockRewriterTest.cs b/test/Microsoft.AspNetCore.Razor.Test/Parser/TagHelpers/Internal/TagHelperBlockRewriterTest.cs index 7cfc585aea..2ceea3d89a 100644 --- a/test/Microsoft.AspNetCore.Razor.Test/Parser/TagHelpers/Internal/TagHelperBlockRewriterTest.cs +++ b/test/Microsoft.AspNetCore.Razor.Test/Parser/TagHelpers/Internal/TagHelperBlockRewriterTest.cs @@ -1013,9 +1013,8 @@ namespace Microsoft.AspNetCore.Razor.Parser.TagHelpers.Internal new ExpressionBlock( factory.CodeTransition(), factory - .Code("DateTime.Now.Year") - .AsImplicitExpression(CSharpCodeParser.DefaultKeywords) - .Accepts(AcceptedCharacters.NonWhiteSpace))))) + .CSharpCodeMarkup("DateTime.Now.Year") + .With(new ExpressionChunkGenerator()))))) })) }, { @@ -1031,14 +1030,10 @@ namespace Microsoft.AspNetCore.Razor.Parser.TagHelpers.Internal new MarkupBlock( factory.CodeMarkup(" "), new ExpressionBlock( + factory.CSharpCodeMarkup("@"), factory - .CodeTransition() - .As(SpanKind.Code) - .With(new MarkupChunkGenerator()), - factory - .Code("DateTime.Now.Year") - .AsImplicitExpression(CSharpCodeParser.DefaultKeywords) - .Accepts(AcceptedCharacters.NonWhiteSpace))))) + .CSharpCodeMarkup("DateTime.Now.Year") + .With(new ExpressionChunkGenerator()))))) })) }, { @@ -1078,14 +1073,9 @@ namespace Microsoft.AspNetCore.Razor.Parser.TagHelpers.Internal new MarkupBlock( factory.CodeMarkup(" "), new ExpressionBlock( - factory - .CodeTransition() - .As(SpanKind.Code) - .With(new MarkupChunkGenerator()), - factory - .Code("value") - .AsImplicitExpression(CSharpCodeParser.DefaultKeywords) - .Accepts(AcceptedCharacters.NonWhiteSpace))), + factory.CSharpCodeMarkup("@"), + factory.CSharpCodeMarkup("value") + .With(new ExpressionChunkGenerator()))), factory.CodeMarkup(" +"), factory.CodeMarkup(" 2"))), new TagHelperAttributeNode( @@ -1094,33 +1084,26 @@ namespace Microsoft.AspNetCore.Razor.Parser.TagHelpers.Internal factory.CodeMarkup("(bool)"), new MarkupBlock( new ExpressionBlock( + factory.CSharpCodeMarkup("@"), factory - .CodeTransition() - .As(SpanKind.Code) - .With(new MarkupChunkGenerator()), - factory - .Code("Bag[\"val\"]") - .AsImplicitExpression(CSharpCodeParser.DefaultKeywords) - .Accepts(AcceptedCharacters.NonWhiteSpace))), + .CSharpCodeMarkup("Bag[\"val\"]") + .With(new ExpressionChunkGenerator()))), factory.CodeMarkup(" ?"), new MarkupBlock( - factory.CodeMarkup(" @").Accepts(AcceptedCharacters.None), + factory.CodeMarkup(" @") + .As(SpanKind.Code), factory.CodeMarkup("@") - .With(SpanChunkGenerator.Null) - .Accepts(AcceptedCharacters.None)), + .As(SpanKind.Code) + .With(SpanChunkGenerator.Null)), factory.CodeMarkup("DateTime"), factory.CodeMarkup(" :"), new MarkupBlock( factory.CodeMarkup(" "), new ExpressionBlock( + factory.CSharpCodeMarkup("@"), factory - .CodeTransition() - .As(SpanKind.Code) - .With(new MarkupChunkGenerator()), - factory - .Code("DateTime.Now") - .AsImplicitExpression(CSharpCodeParser.DefaultKeywords) - .Accepts(AcceptedCharacters.NonWhiteSpace)))), + .CSharpCodeMarkup("DateTime.Now") + .With(new ExpressionChunkGenerator())))), HtmlAttributeValueStyle.SingleQuotes) })) }, @@ -1196,27 +1179,19 @@ namespace Microsoft.AspNetCore.Razor.Parser.TagHelpers.Internal "age", new MarkupBlock( new MarkupBlock( + factory.CodeMarkup("@"), factory.CodeMarkup("@") - .Accepts(AcceptedCharacters.None) - .With(new MarkupChunkGenerator()), - factory.CodeMarkup("@") - .With(SpanChunkGenerator.Null) - .Accepts(AcceptedCharacters.None)), + .With(SpanChunkGenerator.Null)), new MarkupBlock( - factory.EmptyHtml().As(SpanKind.Code), + factory.EmptyHtml() + .AsCodeMarkup() + .As(SpanKind.Code), new ExpressionBlock( - factory.CodeTransition() - .As(SpanKind.Code) - .With(new MarkupChunkGenerator()), - factory.MetaCode("(") - .Accepts(AcceptedCharacters.None) - .As(SpanKind.Code) - .With(new MarkupChunkGenerator()), - factory.Code("11+1").AsExpression(), - factory.MetaCode(")") - .Accepts(AcceptedCharacters.None) - .As(SpanKind.Code) - .With(new MarkupChunkGenerator()))))), + factory.CSharpCodeMarkup("@"), + factory.CSharpCodeMarkup("("), + factory.CSharpCodeMarkup("11+1") + .With(new ExpressionChunkGenerator()), + factory.CSharpCodeMarkup(")"))))), new TagHelperAttributeNode( "birthday", factory.CodeMarkup("DateTime.Now")), @@ -2237,12 +2212,9 @@ namespace Microsoft.AspNetCore.Razor.Parser.TagHelpers.Internal new MarkupBlock( factory.CodeMarkup(" "), new ExpressionBlock( - factory.CodeTransition() - .As(SpanKind.Code) - .With(new MarkupChunkGenerator()), - factory.Code("true") - .AsImplicitExpression(CSharpCodeParser.DefaultKeywords) - .Accepts(AcceptedCharacters.NonWhiteSpace))), + factory.CSharpCodeMarkup("@"), + factory.CSharpCodeMarkup("true") + .With(new ExpressionChunkGenerator()))), factory.CodeMarkup(" ")), HtmlAttributeValueStyle.SingleQuotes) } @@ -2264,18 +2236,11 @@ namespace Microsoft.AspNetCore.Razor.Parser.TagHelpers.Internal new MarkupBlock( factory.CodeMarkup(" "), new ExpressionBlock( - factory.CodeTransition() - .As(SpanKind.Code) - .With(new MarkupChunkGenerator()), - factory.MetaCode("(") - .Accepts(AcceptedCharacters.None) - .As(SpanKind.Code) - .With(new MarkupChunkGenerator()), - factory.Code("true").AsExpression(), - factory.MetaCode(")") - .Accepts(AcceptedCharacters.None) - .As(SpanKind.Code) - .With(new MarkupChunkGenerator()))), + factory.CSharpCodeMarkup("@"), + factory.CSharpCodeMarkup("("), + factory.CSharpCodeMarkup("true") + .With(new ExpressionChunkGenerator()), + factory.CSharpCodeMarkup(")"))), factory.CodeMarkup(" ")), HtmlAttributeValueStyle.SingleQuotes) } diff --git a/test/Microsoft.AspNetCore.Razor.Test/Parser/TagHelpers/Internal/TagHelperParseTreeRewriterTest.cs b/test/Microsoft.AspNetCore.Razor.Test/Parser/TagHelpers/Internal/TagHelperParseTreeRewriterTest.cs index 6b0bf074fb..d329f1ec79 100644 --- a/test/Microsoft.AspNetCore.Razor.Test/Parser/TagHelpers/Internal/TagHelperParseTreeRewriterTest.cs +++ b/test/Microsoft.AspNetCore.Razor.Test/Parser/TagHelpers/Internal/TagHelperParseTreeRewriterTest.cs @@ -7,6 +7,7 @@ using System.Globalization; using System.Linq; using Microsoft.AspNetCore.Razor.Chunks.Generators; using Microsoft.AspNetCore.Razor.Compilation.TagHelpers; +using Microsoft.AspNetCore.Razor.Editor; using Microsoft.AspNetCore.Razor.Parser.Internal; using Microsoft.AspNetCore.Razor.Parser.SyntaxTree; using Microsoft.AspNetCore.Razor.TagHelpers; @@ -2452,8 +2453,8 @@ namespace Microsoft.AspNetCore.Razor.Parser.TagHelpers.Internal new ExpressionBlock( factory.CodeTransition(), factory.Code("DateTime.Now") - .AsImplicitExpression(CSharpCodeParser.DefaultKeywords) - .Accepts(AcceptedCharacters.NonWhiteSpace))))) + .AsImplicitExpression(CSharpCodeParser.DefaultKeywords, acceptTrailingDot: true) + .Accepts(AcceptedCharacters.AnyExceptNewline))))) } })), availableDescriptorsColon @@ -2474,8 +2475,8 @@ namespace Microsoft.AspNetCore.Razor.Parser.TagHelpers.Internal new ExpressionBlock( factory.CodeTransition(), factory.Code("DateTime.Now") - .AsImplicitExpression(CSharpCodeParser.DefaultKeywords) - .Accepts(AcceptedCharacters.NonWhiteSpace))))) + .AsImplicitExpression(CSharpCodeParser.DefaultKeywords, acceptTrailingDot: true) + .Accepts(AcceptedCharacters.AnyExceptNewline))))) } })), availableDescriptorsText @@ -2493,24 +2494,19 @@ namespace Microsoft.AspNetCore.Razor.Parser.TagHelpers.Internal "bound", new MarkupBlock( new MarkupBlock( + factory.CodeMarkup("@"), factory .CodeMarkup("@") - .With(new MarkupChunkGenerator()) - .Accepts(AcceptedCharacters.None), - factory - .CodeMarkup("@") - .With(SpanChunkGenerator.Null) - .Accepts(AcceptedCharacters.None)), + .With(SpanChunkGenerator.Null)), new MarkupBlock( - factory.EmptyHtml().As(SpanKind.Code), + factory + .EmptyHtml() + .As(SpanKind.Code) + .AsCodeMarkup(), new ExpressionBlock( - factory - .CodeTransition() - .As(SpanKind.Code) - .With(new MarkupChunkGenerator()), - factory.Code("DateTime.Now") - .AsImplicitExpression(CSharpCodeParser.DefaultKeywords) - .Accepts(AcceptedCharacters.NonWhiteSpace))))) + factory.CSharpCodeMarkup("@"), + factory.CSharpCodeMarkup("DateTime.Now") + .With(new ExpressionChunkGenerator()))))) } })), availableDescriptorsText diff --git a/test/Microsoft.AspNetCore.Razor.Test/PartialParsingTestBase.cs b/test/Microsoft.AspNetCore.Razor.Test/PartialParsingTestBase.cs index 79655dd3d6..f1b06fd8ae 100644 --- a/test/Microsoft.AspNetCore.Razor.Test/PartialParsingTestBase.cs +++ b/test/Microsoft.AspNetCore.Razor.Test/PartialParsingTestBase.cs @@ -5,6 +5,7 @@ using System; using System.Threading; using System.Web.WebPages.TestUtils; using Microsoft.AspNetCore.Razor.CodeGenerators; +using Microsoft.AspNetCore.Razor.Compilation.TagHelpers; using Microsoft.AspNetCore.Razor.Parser.SyntaxTree; using Microsoft.AspNetCore.Razor.Test.Framework; using Microsoft.AspNetCore.Razor.Test.Utils; @@ -58,7 +59,7 @@ namespace Microsoft.AspNetCore.Razor return new TestParserManager(parser); } - protected static RazorEngineHost CreateHost() + protected static RazorEngineHost CreateHost(ITagHelperDescriptorResolver descriptorResolver = null) { return new RazorEngineHost(new TLanguage()) { @@ -71,7 +72,8 @@ namespace Microsoft.AspNetCore.Razor "Template", "DefineSection", new GeneratedTagHelperContext()), - DesignTimeMode = true + DesignTimeMode = true, + TagHelperDescriptorResolver = descriptorResolver }; } diff --git a/test/Microsoft.AspNetCore.Razor.Test/RazorEditorParserTest.cs b/test/Microsoft.AspNetCore.Razor.Test/RazorEditorParserTest.cs index 34e1e31a6e..c5ad449c94 100644 --- a/test/Microsoft.AspNetCore.Razor.Test/RazorEditorParserTest.cs +++ b/test/Microsoft.AspNetCore.Razor.Test/RazorEditorParserTest.cs @@ -2,17 +2,21 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Collections.Generic; using System.Threading; using System.Web.WebPages.TestUtils; +using Microsoft.AspNetCore.Razor.Compilation.TagHelpers; using Microsoft.AspNetCore.Razor.Editor; using Microsoft.AspNetCore.Razor.Parser; using Microsoft.AspNetCore.Razor.Parser.SyntaxTree; +using Microsoft.AspNetCore.Razor.Parser.TagHelpers; using Microsoft.AspNetCore.Razor.TagHelpers; using Microsoft.AspNetCore.Razor.Test.CodeGenerators; using Microsoft.AspNetCore.Razor.Test.Framework; using Microsoft.AspNetCore.Razor.Test.Utils; using Microsoft.AspNetCore.Razor.Text; using Microsoft.AspNetCore.Testing; +using Moq; using Xunit; namespace Microsoft.AspNetCore.Razor @@ -23,6 +27,273 @@ namespace Microsoft.AspNetCore.Razor private static readonly TestFile SimpleCSHTMLDocumentGenerated = TestFile.Create("TestFiles/DesignTime/Simple.txt"); private const string TestLinePragmaFileName = "C:\\This\\Path\\Is\\Just\\For\\Line\\Pragmas.cshtml"; + public static TheoryData TagHelperPartialParseRejectData + { + get + { + var factory = SpanFactory.CreateCsHtml(); + + // change, expectedDocument + return new TheoryData + { + { + CreateInsertionChange("

", 2, " "), + new MarkupBlock( + new MarkupTagHelperBlock("p")) + }, + { + CreateInsertionChange("

", 6, " "), + new MarkupBlock( + new MarkupTagHelperBlock("p")) + }, + { + CreateInsertionChange("

", 12, " "), + new MarkupBlock( + new MarkupTagHelperBlock( + "p", + attributes: new List + { + new TagHelperAttributeNode( + "some-attr", + value: null, + valueStyle: HtmlAttributeValueStyle.Minimized) + })) + }, + { + CreateInsertionChange("

", 12, "ibute"), + new MarkupBlock( + new MarkupTagHelperBlock( + "p", + attributes: new List + { + new TagHelperAttributeNode( + "some-attribute", + value: null, + valueStyle: HtmlAttributeValueStyle.Minimized) + })) + }, + { + CreateInsertionChange("

", 2, " before"), + new MarkupBlock( + new MarkupTagHelperBlock( + "p", + attributes: new List + { + new TagHelperAttributeNode( + "before", + value: null, + valueStyle: HtmlAttributeValueStyle.Minimized), + new TagHelperAttributeNode( + "some-attr", + value: null, + valueStyle: HtmlAttributeValueStyle.Minimized) + })) + }, + }; + } + } + + [Theory] + [MemberData(nameof(TagHelperPartialParseRejectData))] + public void TagHelperTagBodiesRejectPartialChanges(TextChange change, MarkupBlock expectedDocument) + { + // Arrange + var descriptors = new[] + { + new TagHelperDescriptor + { + TagName = "p", + TypeName = "PTagHelper" + }, + }; + var descriptorResolver = new Mock(); + descriptorResolver + .Setup(resolver => resolver.Resolve(It.IsAny())) + .Returns(descriptors); + var host = CreateHost(descriptorResolver.Object); + var parser = new RazorEditorParser(host, @"C:\This\Is\A\Test\Path"); + + using (var manager = new TestParserManager(parser)) + { + manager.InitializeWithDocument(change.OldBuffer); + + // Act + var result = manager.CheckForStructureChangesAndWait(change); + + // Assert + Assert.Equal(PartialParseResult.Rejected, result); + Assert.Equal(2, manager.ParseCount); + ParserTestBase.EvaluateParseTree(manager.Parser.CurrentParseTree, expectedDocument); + } + } + + public static TheoryData TagHelperAttributeAcceptData + { + get + { + var factory = SpanFactory.CreateCsHtml(); + + // change, expectedDocument, partialParseResult + return new TheoryData + { + { + CreateInsertionChange("

", 22, "."), + new MarkupBlock( + new MarkupTagHelperBlock( + "p", + attributes: new List + { + new TagHelperAttributeNode( + "str-attr", + new MarkupBlock( + new MarkupBlock( + new ExpressionBlock( + factory.CodeTransition(), + factory + .Code("DateTime.") + .AsImplicitExpression(CSharpCodeParser.DefaultKeywords) + .Accepts(AcceptedCharacters.NonWhiteSpace)))), + HtmlAttributeValueStyle.SingleQuotes) + })), + PartialParseResult.Accepted | PartialParseResult.Provisional + }, + { + CreateInsertionChange("

", 21, "."), + new MarkupBlock( + new MarkupTagHelperBlock( + "p", + attributes: new List + { + new TagHelperAttributeNode( + "obj-attr", + factory.CodeMarkup("DateTime."), + HtmlAttributeValueStyle.SingleQuotes) + })), + PartialParseResult.Accepted + }, + { + CreateInsertionChange("

", 25, "."), + new MarkupBlock( + new MarkupTagHelperBlock( + "p", + attributes: new List + { + new TagHelperAttributeNode( + "obj-attr", + factory.CodeMarkup("1 + DateTime."), + HtmlAttributeValueStyle.SingleQuotes) + })), + PartialParseResult.Accepted + }, + { + CreateInsertionChange("

", 34, "."), + new MarkupBlock( + new MarkupTagHelperBlock( + "p", + attributes: new List + { + new TagHelperAttributeNode( + "before-attr", + value: null, + valueStyle: HtmlAttributeValueStyle.Minimized), + new TagHelperAttributeNode( + "str-attr", + new MarkupBlock( + new MarkupBlock( + new ExpressionBlock( + factory.CodeTransition(), + factory + .Code("DateTime.") + .AsImplicitExpression(CSharpCodeParser.DefaultKeywords) + .Accepts(AcceptedCharacters.NonWhiteSpace)))), + HtmlAttributeValueStyle.SingleQuotes), + new TagHelperAttributeNode( + "after-attr", + value: null, + valueStyle: HtmlAttributeValueStyle.Minimized), + })), + PartialParseResult.Accepted | PartialParseResult.Provisional + }, + { + CreateInsertionChange("

", 29, "."), + new MarkupBlock( + new MarkupTagHelperBlock( + "p", + attributes: new List + { + new TagHelperAttributeNode( + "str-attr", + new MarkupBlock( + factory.Markup("before"), + new MarkupBlock( + factory.Markup(" "), + new ExpressionBlock( + factory.CodeTransition(), + factory + .Code("DateTime.") + .AsImplicitExpression(CSharpCodeParser.DefaultKeywords) + .Accepts(AcceptedCharacters.NonWhiteSpace))), + factory.Markup(" after")), + HtmlAttributeValueStyle.SingleQuotes) + })), + PartialParseResult.Accepted | PartialParseResult.Provisional + }, + }; + } + } + + [Theory] + [MemberData(nameof(TagHelperAttributeAcceptData))] + public void TagHelperAttributesAreLocatedAndAcceptChangesCorrectly( + TextChange change, + MarkupBlock expectedDocument, + PartialParseResult partialParseResult) + { + // Arrange + var descriptors = new[] + { + new TagHelperDescriptor + { + TagName = "p", + TypeName = "PTagHelper", + Attributes = new[] + { + new TagHelperAttributeDescriptor + { + Name = "obj-attr", + TypeName = typeof(object).FullName, + PropertyName = "ObjectAttribute", + }, + new TagHelperAttributeDescriptor + { + Name = "str-attr", + TypeName = typeof(string).FullName, + PropertyName = "StringAttribute", + }, + } + }, + }; + var descriptorResolver = new Mock(); + descriptorResolver + .Setup(resolver => resolver.Resolve(It.IsAny())) + .Returns(descriptors); + var host = CreateHost(descriptorResolver.Object); + var parser = new RazorEditorParser(host, @"C:\This\Is\A\Test\Path"); + + using (var manager = new TestParserManager(parser)) + { + manager.InitializeWithDocument(change.OldBuffer); + + // Act + var result = manager.CheckForStructureChangesAndWait(change); + + // Assert + Assert.Equal(partialParseResult, result); + Assert.Equal(1, manager.ParseCount); + ParserTestBase.EvaluateParseTree(manager.Parser.CurrentParseTree, expectedDocument); + } + } + [Fact] public void ConstructorRequiresNonNullPhysicalPath() { @@ -898,5 +1169,14 @@ namespace Microsoft.AspNetCore.Razor { return new CodeGenTestHost(new CSharpRazorCodeLanguage()) { DesignTimeMode = true }; } + + private static TextChange CreateInsertionChange(string initialText, int insertionLocation, string insertionText) + { + var changedText = initialText.Insert(insertionLocation, insertionText); + + var original = new StringTextBuffer(initialText); + var changed = new StringTextBuffer(changedText); + return new TextChange(insertionLocation, 0, original, insertionText.Length, changed); + } } }