diff --git a/src/Microsoft.AspNetCore.Razor/Editor/ImplicitExpressionEditHandler.cs b/src/Microsoft.AspNetCore.Razor/Editor/ImplicitExpressionEditHandler.cs index dc9be897a2..5edeaa8e2e 100644 --- a/src/Microsoft.AspNetCore.Razor/Editor/ImplicitExpressionEditHandler.cs +++ b/src/Microsoft.AspNetCore.Razor/Editor/ImplicitExpressionEditHandler.cs @@ -85,6 +85,11 @@ namespace Microsoft.AspNetCore.Razor.Editor return HandleDotlessCommitInsertion(target); } + if (IsAcceptableIdentifierReplacement(target, normalizedChange)) + { + return TryAcceptChange(target, normalizedChange); + } + if (IsAcceptableReplace(target, normalizedChange)) { return HandleReplacement(target, normalizedChange); @@ -152,7 +157,60 @@ namespace Microsoft.AspNetCore.Razor.Editor private static bool IsAcceptableReplace(Span target, TextChange change) { return IsEndReplace(target, change) || - (change.IsReplace && RemainingIsWhitespace(target, change)); + (change.IsReplace && RemainingIsWhitespace(target, change)); + } + + private bool IsAcceptableIdentifierReplacement(Span target, TextChange change) + { + if (!change.IsReplace) + { + return false; + } + + for (var i = 0; i < target.Symbols.Count; i++) + { + var symbol = target.Symbols[i] as CSharpSymbol; + + if (symbol == null) + { + break; + } + + var symbolStartIndex = target.Start.AbsoluteIndex + symbol.Start.AbsoluteIndex; + var symbolEndIndex = symbolStartIndex + symbol.Content.Length; + + // We're looking for the first symbol that contains the TextChange. + if (symbolEndIndex > change.OldPosition) + { + if (symbolEndIndex >= change.OldPosition + change.OldLength && symbol.Type == CSharpSymbolType.Identifier) + { + // The symbol we're changing happens to be an identifier. Need to check if its transformed state is also one. + // We do this transformation logic to capture the case that the new text change happens to not be an identifier; + // i.e. "5". Alone, it's numeric, within an identifier it's classified as identifier. + var transformedContent = change.ApplyChange(symbol.Content, symbolStartIndex); + var newSymbols = Tokenizer(transformedContent); + + if (newSymbols.Count() != 1) + { + // The transformed content resulted in more than one symbol; we can only replace a single identifier with + // another single identifier. + break; + } + + var newSymbol = (CSharpSymbol)newSymbols.First(); + if (newSymbol.Type == CSharpSymbolType.Identifier) + { + return true; + } + } + + // Change is touching a non-identifier symbol or spans multiple symbols. + + break; + } + } + + return false; } private static bool IsAcceptableDeletion(Span target, TextChange change) diff --git a/test/Microsoft.AspNetCore.Razor.Test/RazorEditorParserTest.cs b/test/Microsoft.AspNetCore.Razor.Test/RazorEditorParserTest.cs index c5ad449c94..e1794ca336 100644 --- a/test/Microsoft.AspNetCore.Razor.Test/RazorEditorParserTest.cs +++ b/test/Microsoft.AspNetCore.Razor.Test/RazorEditorParserTest.cs @@ -576,6 +576,159 @@ namespace Microsoft.AspNetCore.Razor factory.Markup(" baz")), additionalFlags: PartialParseResult.Provisional); } + [Fact] + public void ImplicitExpressionAcceptsWholeIdentifierReplacement() + { + // Arrange + var factory = SpanFactory.CreateCsHtml(); + var old = new StringTextBuffer("foo @date baz"); + var changed = new StringTextBuffer("foo @DateTime baz"); + + // Act and Assert + RunPartialParseTest(new TextChange(5, 4, old, 8, changed), + new MarkupBlock( + factory.Markup("foo "), + new ExpressionBlock( + factory.CodeTransition(), + factory.Code("DateTime").AsImplicitExpression(CSharpCodeParser.DefaultKeywords).Accepts(AcceptedCharacters.NonWhiteSpace)), + factory.Markup(" baz"))); + } + + [Fact] + public void ImplicitExpressionRejectsWholeIdentifierReplacementToKeyword() + { + // Arrange + var host = CreateHost(); + var parser = new RazorEditorParser(host, @"C:\This\Is\A\Test\Path"); + + using (var manager = new TestParserManager(parser)) + { + var old = new StringTextBuffer("foo @date baz"); + var changed = new StringTextBuffer("foo @if baz"); + var textChange = new TextChange(5, 4, old, 2, changed); + manager.InitializeWithDocument(old); + + // Act + var result = manager.CheckForStructureChangesAndWait(textChange); + + // Assert + Assert.Equal(PartialParseResult.Rejected, result); + Assert.Equal(2, manager.ParseCount); + } + } + + [Fact] + public void ImplicitExpressionRejectsWholeIdentifierReplacementToDirective() + { + // Arrange + var host = CreateHost(); + var parser = new RazorEditorParser(host, @"C:\This\Is\A\Test\Path"); + + using (var manager = new TestParserManager(parser)) + { + var old = new StringTextBuffer("foo @date baz"); + var changed = new StringTextBuffer("foo @inherits baz"); + var textChange = new TextChange(5, 4, old, 8, changed); + manager.InitializeWithDocument(old); + + // Act + var result = manager.CheckForStructureChangesAndWait(textChange); + + // Assert + Assert.Equal(PartialParseResult.Rejected | PartialParseResult.SpanContextChanged, result); + Assert.Equal(2, manager.ParseCount); + } + } + + [Fact] + public void ImplicitExpressionAcceptsPrefixIdentifierReplacements_SingleSymbol() + { + // Arrange + var factory = SpanFactory.CreateCsHtml(); + var old = new StringTextBuffer("foo @dTime baz"); + var changed = new StringTextBuffer("foo @DateTime baz"); + + // Act and Assert + RunPartialParseTest(new TextChange(5, 1, old, 4, changed), + new MarkupBlock( + factory.Markup("foo "), + new ExpressionBlock( + factory.CodeTransition(), + factory.Code("DateTime").AsImplicitExpression(CSharpCodeParser.DefaultKeywords).Accepts(AcceptedCharacters.NonWhiteSpace)), + factory.Markup(" baz"))); + } + + [Fact] + public void ImplicitExpressionAcceptsPrefixIdentifierReplacements_MultipleSymbols() + { + // Arrange + var factory = SpanFactory.CreateCsHtml(); + var old = new StringTextBuffer("foo @dTime.Now baz"); + var changed = new StringTextBuffer("foo @DateTime.Now baz"); + + // Act and Assert + RunPartialParseTest(new TextChange(5, 1, old, 4, changed), + new MarkupBlock( + factory.Markup("foo "), + new ExpressionBlock( + factory.CodeTransition(), + factory.Code("DateTime.Now").AsImplicitExpression(CSharpCodeParser.DefaultKeywords).Accepts(AcceptedCharacters.NonWhiteSpace)), + factory.Markup(" baz"))); + } + + [Fact] + public void ImplicitExpressionAcceptsSuffixIdentifierReplacements_SingleSymbol() + { + // Arrange + var factory = SpanFactory.CreateCsHtml(); + var old = new StringTextBuffer("foo @Datet baz"); + var changed = new StringTextBuffer("foo @DateTime baz"); + + // Act and Assert + RunPartialParseTest(new TextChange(9, 1, old, 4, changed), + new MarkupBlock( + factory.Markup("foo "), + new ExpressionBlock( + factory.CodeTransition(), + factory.Code("DateTime").AsImplicitExpression(CSharpCodeParser.DefaultKeywords).Accepts(AcceptedCharacters.NonWhiteSpace)), + factory.Markup(" baz"))); + } + + [Fact] + public void ImplicitExpressionAcceptsSuffixIdentifierReplacements_MultipleSymbols() + { + // Arrange + var factory = SpanFactory.CreateCsHtml(); + var old = new StringTextBuffer("foo @DateTime.n baz"); + var changed = new StringTextBuffer("foo @DateTime.Now baz"); + + // Act and Assert + RunPartialParseTest(new TextChange(14, 1, old, 3, changed), + new MarkupBlock( + factory.Markup("foo "), + new ExpressionBlock( + factory.CodeTransition(), + factory.Code("DateTime.Now").AsImplicitExpression(CSharpCodeParser.DefaultKeywords).Accepts(AcceptedCharacters.NonWhiteSpace)), + factory.Markup(" baz"))); + } + + [Fact] + public void ImplicitExpressionAcceptsSurroundedIdentifierReplacements() + { + // Arrange + var factory = SpanFactory.CreateCsHtml(); + var old = new StringTextBuffer("foo @DateTime.n.ToString() baz"); + var changed = new StringTextBuffer("foo @DateTime.Now.ToString() baz"); + + // Act and Assert + RunPartialParseTest(new TextChange(14, 1, old, 3, changed), + new MarkupBlock( + factory.Markup("foo "), + new ExpressionBlock( + factory.CodeTransition(), + factory.Code("DateTime.Now.ToString()").AsImplicitExpression(CSharpCodeParser.DefaultKeywords).Accepts(AcceptedCharacters.NonWhiteSpace)), + factory.Markup(" baz"))); + } [Fact] public void ImplicitExpressionAcceptsDotlessCommitInsertionsInStatementBlockAfterIdentifiers() @@ -775,6 +928,61 @@ namespace Microsoft.AspNetCore.Razor } } + [Fact] + public void ImplicitExpressionProvisionallyAcceptsCaseInsensitiveDotlessCommitInsertions_NewRoslynIntegration() + { + var factory = SpanFactory.CreateCsHtml(); + var old = new StringTextBuffer("foo @date baz"); + var changed = new StringTextBuffer("foo @date. baz"); + var textChange = new TextChange(9, 0, old, 1, changed); + using (var manager = CreateParserManager()) + { + Action applyAndVerifyPartialChange = (changeToApply, expectedResult, expectedCode) => + { + var result = manager.CheckForStructureChangesAndWait(textChange); + + // Assert + Assert.Equal(expectedResult, result); + Assert.Equal(1, manager.ParseCount); + + ParserTestBase.EvaluateParseTree(manager.Parser.CurrentParseTree, new MarkupBlock( + factory.Markup("foo "), + new ExpressionBlock( + factory.CodeTransition(), + factory.Code(expectedCode).AsImplicitExpression(CSharpCodeParser.DefaultKeywords).Accepts(AcceptedCharacters.NonWhiteSpace)), + factory.Markup(" baz"))); + }; + + manager.InitializeWithDocument(textChange.OldBuffer); + + // This is the process of a dotless commit when doing "." insertions to commit intellisense changes. + + // @date => @date. + applyAndVerifyPartialChange(textChange, PartialParseResult.Accepted | PartialParseResult.Provisional, "date."); + + old = changed; + changed = new StringTextBuffer("foo @date baz"); + textChange = new TextChange(9, 1, old, 0, changed); + + // @date. => @date + applyAndVerifyPartialChange(textChange, PartialParseResult.Accepted, "date"); + + old = changed; + changed = new StringTextBuffer("foo @DateTime baz"); + textChange = new TextChange(5, 4, old, 8, changed); + + // @date => @DateTime + applyAndVerifyPartialChange(textChange, PartialParseResult.Accepted, "DateTime"); + + old = changed; + changed = new StringTextBuffer("foo @DateTime. baz"); + textChange = new TextChange(13, 0, old, 1, changed); + + // @DateTime => @DateTime. + applyAndVerifyPartialChange(textChange, PartialParseResult.Accepted | PartialParseResult.Provisional, "DateTime."); + } + } + [Fact] public void ImplicitExpressionProvisionallyAcceptsDeleteOfIdentifierPartsIfDotRemains() {