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:
parent
7af2f6ff36
commit
c49d7b8c27
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in New Issue