Add support for new style Roslyn dotless commits.

- Roslyn swapped the way they performed dotless commit insertions. They went from:
date => date. => DateTime.  to
date => date. => date => DateTime => DateTime.
The problem with the new approach is that date => DateTime would be rejected and therefore force the editor to reparse and reclassify any dots as HTML giving improper IntelliSense.
- Updated Razor implicit expression edit handling to allow identifier => identifier replacements as long as the identifiers didn't result in keyword or directives.
- Added tests to verify the scenarios impacted.
This commit is contained in:
N. Taylor Mullen 2017-01-23 17:13:13 -08:00
parent 7af2f6ff36
commit c49d7b8c27
2 changed files with 267 additions and 1 deletions

View File

@ -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)

View File

@ -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<TextChange, PartialParseResult, string> 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()
{