Skip to content

Commit 3ad7135

Browse files
author
N. Taylor Mullen
committed
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.
1 parent f830808 commit 3ad7135

File tree

2 files changed

+266
-0
lines changed

2 files changed

+266
-0
lines changed

src/System.Web.Razor/Editor/ImplicitExpressionEditHandler.cs

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,11 @@ protected override PartialParseResult CanAcceptChange(Span target, TextChange no
6969
return HandleDotlessCommitInsertion(target);
7070
}
7171

72+
if (IsAcceptableIdentifierReplacement(target, normalizedChange))
73+
{
74+
return TryAcceptChange(target, normalizedChange);
75+
}
76+
7277
if (IsAcceptableReplace(target, normalizedChange))
7378
{
7479
return HandleReplacement(target, normalizedChange);
@@ -139,6 +144,59 @@ private static bool IsSecondaryDotlessCommitInsertion(Span target, TextChange ch
139144
change.OldLength == 0;
140145
}
141146

147+
private bool IsAcceptableIdentifierReplacement(Span target, TextChange change)
148+
{
149+
if (!change.IsReplace)
150+
{
151+
return false;
152+
}
153+
154+
foreach (ISymbol isymbol in target.Symbols)
155+
{
156+
CSharpSymbol symbol = isymbol as CSharpSymbol;
157+
158+
if (symbol == null)
159+
{
160+
break;
161+
}
162+
163+
int symbolStartIndex = target.Start.AbsoluteIndex + symbol.Start.AbsoluteIndex;
164+
int symbolEndIndex = symbolStartIndex + symbol.Content.Length;
165+
166+
// We're looking for the first symbol that contains the TextChange.
167+
if (symbolEndIndex > change.OldPosition)
168+
{
169+
if (symbolEndIndex >= change.OldPosition + change.OldLength && symbol.Type == CSharpSymbolType.Identifier)
170+
{
171+
// The symbol we're changing happens to be an identifier. Need to check if its transformed state is also one.
172+
// We do this transformation logic to capture the case that the new text change happens to not be an identifier;
173+
// i.e. "5". Alone, it's numeric, within an identifier it's classified as identifier.
174+
string transformedContent = change.ApplyChange(symbol.Content, symbolStartIndex);
175+
IEnumerable<ISymbol> newSymbols = Tokenizer(transformedContent);
176+
177+
if (newSymbols.Count() != 1)
178+
{
179+
// The transformed content resulted in more than one symbol; we can only replace a single identifier with
180+
// another single identifier.
181+
break;
182+
}
183+
184+
CSharpSymbol newSymbol = (CSharpSymbol)newSymbols.First();
185+
if (newSymbol.Type == CSharpSymbolType.Identifier)
186+
{
187+
return true;
188+
}
189+
}
190+
191+
// Change is touching a non-identifier symbol or spans multiple symbols.
192+
193+
break;
194+
}
195+
}
196+
197+
return false;
198+
}
199+
142200
private static bool IsAcceptableReplace(Span target, TextChange change)
143201
{
144202
return IsEndReplace(target, change) ||

test/System.Web.Razor.Test/Parser/PartialParsing/CSharpPartialParsingTest.cs

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,159 @@ public void ImplicitExpressionAcceptsInnerInsertions()
6060
factory.Markup(" baz")), additionalFlags: PartialParseResult.Provisional);
6161
}
6262

63+
[Fact]
64+
public void ImplicitExpressionAcceptsWholeIdentifierReplacement()
65+
{
66+
// Arrange
67+
SpanFactory factory = SpanFactory.CreateCsHtml();
68+
StringTextBuffer old = new StringTextBuffer("foo @date baz");
69+
StringTextBuffer changed = new StringTextBuffer("foo @DateTime baz");
70+
71+
// Act and Assert
72+
RunPartialParseTest(new TextChange(5, 4, old, 8, changed),
73+
new MarkupBlock(
74+
factory.Markup("foo "),
75+
new ExpressionBlock(
76+
factory.CodeTransition(),
77+
factory.Code("DateTime").AsImplicitExpression(CSharpCodeParser.DefaultKeywords).Accepts(AcceptedCharacters.NonWhiteSpace)),
78+
factory.Markup(" baz")));
79+
}
80+
81+
[Fact]
82+
public void ImplicitExpressionRejectsWholeIdentifierReplacementToKeyword()
83+
{
84+
// Arrange
85+
RazorEngineHost host = CreateHost();
86+
RazorEditorParser parser = new RazorEditorParser(host, @"C:\This\Is\A\Test\Path");
87+
88+
using (TestParserManager manager = new TestParserManager(parser))
89+
{
90+
StringTextBuffer old = new StringTextBuffer("foo @date baz");
91+
StringTextBuffer changed = new StringTextBuffer("foo @if baz");
92+
TextChange textChange = new TextChange(5, 4, old, 2, changed);
93+
manager.InitializeWithDocument(old);
94+
95+
// Act
96+
PartialParseResult result = manager.CheckForStructureChangesAndWait(textChange);
97+
98+
// Assert
99+
Assert.Equal(PartialParseResult.Rejected, result);
100+
Assert.Equal(2, manager.ParseCount);
101+
}
102+
}
103+
104+
[Fact]
105+
public void ImplicitExpressionRejectsWholeIdentifierReplacementToDirective()
106+
{
107+
// Arrange
108+
RazorEngineHost host = CreateHost();
109+
RazorEditorParser parser = new RazorEditorParser(host, @"C:\This\Is\A\Test\Path");
110+
111+
using (var manager = new TestParserManager(parser))
112+
{
113+
StringTextBuffer old = new StringTextBuffer("foo @date baz");
114+
StringTextBuffer changed = new StringTextBuffer("foo @inherits baz");
115+
TextChange textChange = new TextChange(5, 4, old, 8, changed);
116+
manager.InitializeWithDocument(old);
117+
118+
// Act
119+
PartialParseResult result = manager.CheckForStructureChangesAndWait(textChange);
120+
121+
// Assert
122+
Assert.Equal(PartialParseResult.Rejected | PartialParseResult.SpanContextChanged, result);
123+
Assert.Equal(2, manager.ParseCount);
124+
}
125+
}
126+
127+
[Fact]
128+
public void ImplicitExpressionAcceptsPrefixIdentifierReplacements_SingleSymbol()
129+
{
130+
// Arrange
131+
SpanFactory factory = SpanFactory.CreateCsHtml();
132+
StringTextBuffer old = new StringTextBuffer("foo @dTime baz");
133+
StringTextBuffer changed = new StringTextBuffer("foo @DateTime baz");
134+
135+
// Act and Assert
136+
RunPartialParseTest(new TextChange(5, 1, old, 4, changed),
137+
new MarkupBlock(
138+
factory.Markup("foo "),
139+
new ExpressionBlock(
140+
factory.CodeTransition(),
141+
factory.Code("DateTime").AsImplicitExpression(CSharpCodeParser.DefaultKeywords).Accepts(AcceptedCharacters.NonWhiteSpace)),
142+
factory.Markup(" baz")));
143+
}
144+
145+
[Fact]
146+
public void ImplicitExpressionAcceptsPrefixIdentifierReplacements_MultipleSymbols()
147+
{
148+
// Arrange
149+
SpanFactory factory = SpanFactory.CreateCsHtml();
150+
StringTextBuffer old = new StringTextBuffer("foo @dTime.Now baz");
151+
StringTextBuffer changed = new StringTextBuffer("foo @DateTime.Now baz");
152+
153+
// Act and Assert
154+
RunPartialParseTest(new TextChange(5, 1, old, 4, changed),
155+
new MarkupBlock(
156+
factory.Markup("foo "),
157+
new ExpressionBlock(
158+
factory.CodeTransition(),
159+
factory.Code("DateTime.Now").AsImplicitExpression(CSharpCodeParser.DefaultKeywords).Accepts(AcceptedCharacters.NonWhiteSpace)),
160+
factory.Markup(" baz")));
161+
}
162+
163+
[Fact]
164+
public void ImplicitExpressionAcceptsSuffixIdentifierReplacements_SingleSymbol()
165+
{
166+
// Arrange
167+
SpanFactory factory = SpanFactory.CreateCsHtml();
168+
StringTextBuffer old = new StringTextBuffer("foo @Datet baz");
169+
StringTextBuffer changed = new StringTextBuffer("foo @DateTime baz");
170+
171+
// Act and Assert
172+
RunPartialParseTest(new TextChange(9, 1, old, 4, changed),
173+
new MarkupBlock(
174+
factory.Markup("foo "),
175+
new ExpressionBlock(
176+
factory.CodeTransition(),
177+
factory.Code("DateTime").AsImplicitExpression(CSharpCodeParser.DefaultKeywords).Accepts(AcceptedCharacters.NonWhiteSpace)),
178+
factory.Markup(" baz")));
179+
}
180+
181+
[Fact]
182+
public void ImplicitExpressionAcceptsSuffixIdentifierReplacements_MultipleSymbols()
183+
{
184+
// Arrange
185+
SpanFactory factory = SpanFactory.CreateCsHtml();
186+
StringTextBuffer old = new StringTextBuffer("foo @DateTime.n baz");
187+
StringTextBuffer changed = new StringTextBuffer("foo @DateTime.Now baz");
188+
189+
// Act and Assert
190+
RunPartialParseTest(new TextChange(14, 1, old, 3, changed),
191+
new MarkupBlock(
192+
factory.Markup("foo "),
193+
new ExpressionBlock(
194+
factory.CodeTransition(),
195+
factory.Code("DateTime.Now").AsImplicitExpression(CSharpCodeParser.DefaultKeywords).Accepts(AcceptedCharacters.NonWhiteSpace)),
196+
factory.Markup(" baz")));
197+
}
198+
199+
[Fact]
200+
public void ImplicitExpressionAcceptsSurroundedIdentifierReplacements()
201+
{
202+
// Arrange
203+
SpanFactory factory = SpanFactory.CreateCsHtml();
204+
StringTextBuffer old = new StringTextBuffer("foo @DateTime.n.ToString() baz");
205+
StringTextBuffer changed = new StringTextBuffer("foo @DateTime.Now.ToString() baz");
206+
207+
// Act and Assert
208+
RunPartialParseTest(new TextChange(14, 1, old, 3, changed),
209+
new MarkupBlock(
210+
factory.Markup("foo "),
211+
new ExpressionBlock(
212+
factory.CodeTransition(),
213+
factory.Code("DateTime.Now.ToString()").AsImplicitExpression(CSharpCodeParser.DefaultKeywords).Accepts(AcceptedCharacters.NonWhiteSpace)),
214+
factory.Markup(" baz")));
215+
}
63216

64217
[Fact]
65218
public void ImplicitExpressionAcceptsDotlessCommitInsertionsInStatementBlockAfterIdentifiers()
@@ -255,6 +408,61 @@ public void ImplicitExpressionProvisionallyAcceptsDotlessCommitInsertionsAfterId
255408
}
256409
}
257410

411+
[Fact]
412+
public void ImplicitExpressionProvisionallyAcceptsCaseInsensitiveDotlessCommitInsertions_NewRoslynIntegration()
413+
{
414+
SpanFactory factory = SpanFactory.CreateCsHtml();
415+
StringTextBuffer old = new StringTextBuffer("foo @date baz");
416+
StringTextBuffer changed = new StringTextBuffer("foo @date. baz");
417+
TextChange textChange = new TextChange(9, 0, old, 1, changed);
418+
using (TestParserManager manager = CreateParserManager())
419+
{
420+
Action<TextChange, PartialParseResult, string> applyAndVerifyPartialChange = (changeToApply, expectedResult, expectedCode) =>
421+
{
422+
PartialParseResult result = manager.CheckForStructureChangesAndWait(textChange);
423+
424+
// Assert
425+
Assert.Equal(expectedResult, result);
426+
Assert.Equal(1, manager.ParseCount);
427+
428+
ParserTestBase.EvaluateParseTree(manager.Parser.CurrentParseTree, new MarkupBlock(
429+
factory.Markup("foo "),
430+
new ExpressionBlock(
431+
factory.CodeTransition(),
432+
factory.Code(expectedCode).AsImplicitExpression(CSharpCodeParser.DefaultKeywords).Accepts(AcceptedCharacters.NonWhiteSpace)),
433+
factory.Markup(" baz")));
434+
};
435+
436+
manager.InitializeWithDocument(textChange.OldBuffer);
437+
438+
// This is the process of a dotless commit when doing "." insertions to commit intellisense changes.
439+
440+
// @date => @date.
441+
applyAndVerifyPartialChange(textChange, PartialParseResult.Accepted | PartialParseResult.Provisional, "date.");
442+
443+
old = changed;
444+
changed = new StringTextBuffer("foo @date baz");
445+
textChange = new TextChange(9, 1, old, 0, changed);
446+
447+
// @date. => @date
448+
applyAndVerifyPartialChange(textChange, PartialParseResult.Accepted, "date");
449+
450+
old = changed;
451+
changed = new StringTextBuffer("foo @DateTime baz");
452+
textChange = new TextChange(5, 4, old, 8, changed);
453+
454+
// @date => @DateTime
455+
applyAndVerifyPartialChange(textChange, PartialParseResult.Accepted, "DateTime");
456+
457+
old = changed;
458+
changed = new StringTextBuffer("foo @DateTime. baz");
459+
textChange = new TextChange(13, 0, old, 1, changed);
460+
461+
// @DateTime => @DateTime.
462+
applyAndVerifyPartialChange(textChange, PartialParseResult.Accepted | PartialParseResult.Provisional, "DateTime.");
463+
}
464+
}
465+
258466
[Fact]
259467
public void ImplicitExpressionProvisionallyAcceptsDeleteOfIdentifierPartsIfDotRemains()
260468
{

0 commit comments

Comments
 (0)