From f8d43853f89b7ebbc71d6e4d3b65b00126f768e2 Mon Sep 17 00:00:00 2001
From: "N. Taylor Mullen"
Date: Mon, 10 Jul 2017 17:21:15 -0700
Subject: [PATCH] Re-introduce RazorEditorParser.
- Revived `RazorEditorParser`.
- Made `PartialParseResult` internal and renamed it to `PartialParseResultInternal`. This fell in line with other syntax tree types.
- Moved the `RazorEditorParser` implementation away from `TextChange` and `ITextBuffer`. Instead it now relies on `SourceChange` and the VS contract `ITextSnapshot`.
- Added `RazorEditorParserTest` to ensure the changes in implementation did not impact previous functionality.
- Removed some obvious tests that unnecessarily re-tested behavior that was already verified.
- Updated tests.
- Moved several Language.Test types to the common test project so they could be reused.
#1259
---
.../DirectiveTokenEditHandler.cs | 6 +-
.../Legacy/AutoCompleteEditHandler.cs | 6 +-
.../Legacy/EditResult.cs | 4 +-
.../Legacy/ImplicitExpressionEditHandler.cs | 48 +-
...esult.cs => PartialParseResultInternal.cs} | 2 +-
.../Legacy/SpanEditHandler.cs | 8 +-
.../BackgroundParser.cs | 419 +++++
.../DocumentParseCompleteEventArgs.cs | 47 +
.../PartialParseResult.cs | 21 +
.../RazorEditorParser.cs | 187 +++
.../TextSnapshotSourceDocument.cs | 79 +
.../DirectiveTokenEditHandlerTest.cs | 6 +-
.../Legacy/StringTextBuffer.cs | 53 -
.../TestFiles/DesignTime/Simple.cshtml | 16 -
.../TestFiles/DesignTime/Simple.txt | 43 -
.../Language}/Legacy/BlockExtensions.cs | 0
.../Language}/Legacy/BlockFactory.cs | 0
.../Language}/Legacy/BlockTypes.cs | 0
.../Language}/Legacy/ErrorCollector.cs | 0
.../Language}/Legacy/ParserTestBase.cs | 0
.../Language}/Legacy/RawTextSymbol.cs | 0
.../Language}/Legacy/TestSpanBuilder.cs | 0
.../Language}/SyntaxTreeVerifier.cs | 0
...lStudio.LanguageServices.Razor.Test.csproj | 1 +
.../RazorEditorParserTest.cs | 1479 +++++++++++++++++
.../StringTextSnapshot.cs | 106 ++
...undAttributeDescriptorBuilderExtensions.cs | 3 +-
...redAttributeDescriptorBuilderExtensions.cs | 3 +-
...estTagHelperDescriptorBuilderExtensions.cs | 3 +-
...MatchingRuleDescriptorBuilderExtensions.cs | 3 +-
30 files changed, 2387 insertions(+), 156 deletions(-)
rename src/Microsoft.AspNetCore.Razor.Language/Legacy/{PartialParseResult.cs => PartialParseResultInternal.cs} (98%)
create mode 100644 src/Microsoft.VisualStudio.LanguageServices.Razor/BackgroundParser.cs
create mode 100644 src/Microsoft.VisualStudio.LanguageServices.Razor/DocumentParseCompleteEventArgs.cs
create mode 100644 src/Microsoft.VisualStudio.LanguageServices.Razor/PartialParseResult.cs
create mode 100644 src/Microsoft.VisualStudio.LanguageServices.Razor/RazorEditorParser.cs
create mode 100644 src/Microsoft.VisualStudio.LanguageServices.Razor/TextSnapshotSourceDocument.cs
delete mode 100644 test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/StringTextBuffer.cs
delete mode 100644 test/Microsoft.AspNetCore.Razor.Language.Test/TestFiles/DesignTime/Simple.cshtml
delete mode 100644 test/Microsoft.AspNetCore.Razor.Language.Test/TestFiles/DesignTime/Simple.txt
rename test/{Microsoft.AspNetCore.Razor.Language.Test => Microsoft.AspNetCore.Razor.Test.Common/Language}/Legacy/BlockExtensions.cs (100%)
rename test/{Microsoft.AspNetCore.Razor.Language.Test => Microsoft.AspNetCore.Razor.Test.Common/Language}/Legacy/BlockFactory.cs (100%)
rename test/{Microsoft.AspNetCore.Razor.Language.Test => Microsoft.AspNetCore.Razor.Test.Common/Language}/Legacy/BlockTypes.cs (100%)
rename test/{Microsoft.AspNetCore.Razor.Language.Test => Microsoft.AspNetCore.Razor.Test.Common/Language}/Legacy/ErrorCollector.cs (100%)
rename test/{Microsoft.AspNetCore.Razor.Language.Test => Microsoft.AspNetCore.Razor.Test.Common/Language}/Legacy/ParserTestBase.cs (100%)
rename test/{Microsoft.AspNetCore.Razor.Language.Test => Microsoft.AspNetCore.Razor.Test.Common/Language}/Legacy/RawTextSymbol.cs (100%)
rename test/{Microsoft.AspNetCore.Razor.Language.Test => Microsoft.AspNetCore.Razor.Test.Common/Language}/Legacy/TestSpanBuilder.cs (100%)
rename test/{Microsoft.AspNetCore.Razor.Language.Test => Microsoft.AspNetCore.Razor.Test.Common/Language}/SyntaxTreeVerifier.cs (100%)
create mode 100644 test/Microsoft.VisualStudio.LanguageServices.Razor.Test/RazorEditorParserTest.cs
create mode 100644 test/Microsoft.VisualStudio.LanguageServices.Razor.Test/StringTextSnapshot.cs
rename test/Microsoft.VisualStudio.LanguageServices.Razor.Test/{Language => Test}/TestBoundAttributeDescriptorBuilderExtensions.cs (97%)
rename test/Microsoft.VisualStudio.LanguageServices.Razor.Test/{Language => Test}/TestRequiredAttributeDescriptorBuilderExtensions.cs (95%)
rename test/Microsoft.VisualStudio.LanguageServices.Razor.Test/{Language => Test}/TestTagHelperDescriptorBuilderExtensions.cs (97%)
rename test/Microsoft.VisualStudio.LanguageServices.Razor.Test/{Language => Test}/TestTagMatchingRuleDescriptorBuilderExtensions.cs (95%)
diff --git a/src/Microsoft.AspNetCore.Razor.Language/DirectiveTokenEditHandler.cs b/src/Microsoft.AspNetCore.Razor.Language/DirectiveTokenEditHandler.cs
index 5c3f4403e0..6abaa03f5c 100644
--- a/src/Microsoft.AspNetCore.Razor.Language/DirectiveTokenEditHandler.cs
+++ b/src/Microsoft.AspNetCore.Razor.Language/DirectiveTokenEditHandler.cs
@@ -13,7 +13,7 @@ namespace Microsoft.AspNetCore.Razor.Language
{
}
- protected override PartialParseResult CanAcceptChange(Span target, SourceChange change)
+ protected override PartialParseResultInternal CanAcceptChange(Span target, SourceChange change)
{
if (AcceptedCharacters == AcceptedCharactersInternal.NonWhiteSpace)
{
@@ -25,11 +25,11 @@ namespace Microsoft.AspNetCore.Razor.Language
// Did not modify whitespace, directive format should be the same.
// Return provisional so extensible IR/code gen pieces can see the full directive text
// once the user stops editing the document.
- return PartialParseResult.Accepted | PartialParseResult.Provisional;
+ return PartialParseResultInternal.Accepted | PartialParseResultInternal.Provisional;
}
}
- return PartialParseResult.Rejected;
+ return PartialParseResultInternal.Rejected;
}
diff --git a/src/Microsoft.AspNetCore.Razor.Language/Legacy/AutoCompleteEditHandler.cs b/src/Microsoft.AspNetCore.Razor.Language/Legacy/AutoCompleteEditHandler.cs
index ae4291f987..bd73bbc1b2 100644
--- a/src/Microsoft.AspNetCore.Razor.Language/Legacy/AutoCompleteEditHandler.cs
+++ b/src/Microsoft.AspNetCore.Razor.Language/Legacy/AutoCompleteEditHandler.cs
@@ -31,16 +31,16 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy
public string AutoCompleteString { get; set; }
- protected override PartialParseResult CanAcceptChange(Span target, SourceChange change)
+ protected override PartialParseResultInternal CanAcceptChange(Span target, SourceChange change)
{
if (((AutoCompleteAtEndOfSpan && IsAtEndOfSpan(target, change)) || IsAtEndOfFirstLine(target, change)) &&
change.IsInsert &&
ParserHelpers.IsNewLine(change.NewText) &&
AutoCompleteString != null)
{
- return PartialParseResult.Rejected | PartialParseResult.AutoCompleteBlock;
+ return PartialParseResultInternal.Rejected | PartialParseResultInternal.AutoCompleteBlock;
}
- return PartialParseResult.Rejected;
+ return PartialParseResultInternal.Rejected;
}
public override string ToString()
diff --git a/src/Microsoft.AspNetCore.Razor.Language/Legacy/EditResult.cs b/src/Microsoft.AspNetCore.Razor.Language/Legacy/EditResult.cs
index 351116e090..6aff8dea24 100644
--- a/src/Microsoft.AspNetCore.Razor.Language/Legacy/EditResult.cs
+++ b/src/Microsoft.AspNetCore.Razor.Language/Legacy/EditResult.cs
@@ -5,13 +5,13 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy
{
internal class EditResult
{
- public EditResult(PartialParseResult result, SpanBuilder editedSpan)
+ public EditResult(PartialParseResultInternal result, SpanBuilder editedSpan)
{
Result = result;
EditedSpan = editedSpan;
}
- public PartialParseResult Result { get; set; }
+ public PartialParseResultInternal Result { get; set; }
public SpanBuilder EditedSpan { get; set; }
}
}
diff --git a/src/Microsoft.AspNetCore.Razor.Language/Legacy/ImplicitExpressionEditHandler.cs b/src/Microsoft.AspNetCore.Razor.Language/Legacy/ImplicitExpressionEditHandler.cs
index 82b9b5db19..ababd6c7f7 100644
--- a/src/Microsoft.AspNetCore.Razor.Language/Legacy/ImplicitExpressionEditHandler.cs
+++ b/src/Microsoft.AspNetCore.Razor.Language/Legacy/ImplicitExpressionEditHandler.cs
@@ -59,11 +59,11 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy
return hashCodeCombiner;
}
- protected override PartialParseResult CanAcceptChange(Span target, SourceChange change)
+ protected override PartialParseResultInternal CanAcceptChange(Span target, SourceChange change)
{
if (AcceptedCharacters == AcceptedCharactersInternal.Any)
{
- return PartialParseResult.Rejected;
+ return PartialParseResultInternal.Rejected;
}
// In some editors intellisense insertions are handled as "dotless commits". If an intellisense selection is confirmed
@@ -99,7 +99,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy
// Don't support 0->1 length edits
if (lastChar == null)
{
- return PartialParseResult.Rejected;
+ return PartialParseResultInternal.Rejected;
}
// Accepts cases when insertions are made at the end of a span or '.' is inserted within a span.
@@ -114,7 +114,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy
return HandleDeletion(target, lastChar.Value, change);
}
- return PartialParseResult.Rejected;
+ return PartialParseResultInternal.Rejected;
}
// A dotless commit is the process of inserting a '.' with an intellisense selection.
@@ -249,17 +249,17 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy
return string.IsNullOrWhiteSpace(target.Content.Substring(offset));
}
- private PartialParseResult HandleDotlessCommitInsertion(Span target)
+ private PartialParseResultInternal HandleDotlessCommitInsertion(Span target)
{
- var result = PartialParseResult.Accepted;
+ var result = PartialParseResultInternal.Accepted;
if (!AcceptTrailingDot && target.Content.LastOrDefault() == '.')
{
- result |= PartialParseResult.Provisional;
+ result |= PartialParseResultInternal.Provisional;
}
return result;
}
- private PartialParseResult HandleReplacement(Span target, SourceChange change)
+ private PartialParseResultInternal HandleReplacement(Span target, SourceChange change)
{
// Special Case for IntelliSense commits.
// When IntelliSense commits, we get two changes (for example user typed "Date", then committed "DateTime" by pressing ".")
@@ -268,24 +268,24 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy
// We need partial parsing to accept case #2.
var oldText = change.GetOriginalText(target);
- var result = PartialParseResult.Rejected;
+ var result = PartialParseResultInternal.Rejected;
if (EndsWithDot(oldText) && EndsWithDot(change.NewText))
{
- result = PartialParseResult.Accepted;
+ result = PartialParseResultInternal.Accepted;
if (!AcceptTrailingDot)
{
- result |= PartialParseResult.Provisional;
+ result |= PartialParseResultInternal.Provisional;
}
}
return result;
}
- private PartialParseResult HandleDeletion(Span target, char previousChar, SourceChange change)
+ private PartialParseResultInternal HandleDeletion(Span target, char previousChar, SourceChange change)
{
// What's left after deleting?
if (previousChar == '.')
{
- return TryAcceptChange(target, change, PartialParseResult.Accepted | PartialParseResult.Provisional);
+ return TryAcceptChange(target, change, PartialParseResultInternal.Accepted | PartialParseResultInternal.Provisional);
}
else if (ParserHelpers.IsIdentifierPart(previousChar))
{
@@ -293,11 +293,11 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy
}
else
{
- return PartialParseResult.Rejected;
+ return PartialParseResultInternal.Rejected;
}
}
- private PartialParseResult HandleInsertion(Span target, char previousChar, SourceChange change)
+ private PartialParseResultInternal HandleInsertion(Span target, char previousChar, SourceChange change)
{
// What are we inserting after?
if (previousChar == '.')
@@ -310,11 +310,11 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy
}
else
{
- return PartialParseResult.Rejected;
+ return PartialParseResultInternal.Rejected;
}
}
- private PartialParseResult HandleInsertionAfterIdPart(Span target, SourceChange change)
+ private PartialParseResultInternal HandleInsertionAfterIdPart(Span target, SourceChange change)
{
// If the insertion is a full identifier part, accept it
if (ParserHelpers.IsIdentifier(change.NewText, requireIdentifierStart: false))
@@ -324,16 +324,16 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy
else if (EndsWithDot(change.NewText))
{
// Accept it, possibly provisionally
- var result = PartialParseResult.Accepted;
+ var result = PartialParseResultInternal.Accepted;
if (!AcceptTrailingDot)
{
- result |= PartialParseResult.Provisional;
+ result |= PartialParseResultInternal.Provisional;
}
return TryAcceptChange(target, change, result);
}
else
{
- return PartialParseResult.Rejected;
+ return PartialParseResultInternal.Rejected;
}
}
@@ -344,22 +344,22 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy
content.Take(content.Length - 1).All(ParserHelpers.IsIdentifierPart));
}
- private PartialParseResult HandleInsertionAfterDot(Span target, SourceChange change)
+ private PartialParseResultInternal HandleInsertionAfterDot(Span target, SourceChange change)
{
// If the insertion is a full identifier or another dot, accept it
if (ParserHelpers.IsIdentifier(change.NewText) || change.NewText == ".")
{
return TryAcceptChange(target, change);
}
- return PartialParseResult.Rejected;
+ return PartialParseResultInternal.Rejected;
}
- private PartialParseResult TryAcceptChange(Span target, SourceChange change, PartialParseResult acceptResult = PartialParseResult.Accepted)
+ private PartialParseResultInternal TryAcceptChange(Span target, SourceChange change, PartialParseResultInternal acceptResult = PartialParseResultInternal.Accepted)
{
var content = change.GetEditedContent(target);
if (StartsWithKeyword(content))
{
- return PartialParseResult.Rejected | PartialParseResult.SpanContextChanged;
+ return PartialParseResultInternal.Rejected | PartialParseResultInternal.SpanContextChanged;
}
return acceptResult;
diff --git a/src/Microsoft.AspNetCore.Razor.Language/Legacy/PartialParseResult.cs b/src/Microsoft.AspNetCore.Razor.Language/Legacy/PartialParseResultInternal.cs
similarity index 98%
rename from src/Microsoft.AspNetCore.Razor.Language/Legacy/PartialParseResult.cs
rename to src/Microsoft.AspNetCore.Razor.Language/Legacy/PartialParseResultInternal.cs
index 0b61553b53..a6a79c01bf 100644
--- a/src/Microsoft.AspNetCore.Razor.Language/Legacy/PartialParseResult.cs
+++ b/src/Microsoft.AspNetCore.Razor.Language/Legacy/PartialParseResultInternal.cs
@@ -22,7 +22,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy
/// Provisional may NOT be set with Rejected and SpanContextChanged may NOT be set with Accepted.
///
[Flags]
- internal enum PartialParseResult
+ internal enum PartialParseResultInternal
{
///
/// Indicates that the edit could not be accepted and that a reparse is underway.
diff --git a/src/Microsoft.AspNetCore.Razor.Language/Legacy/SpanEditHandler.cs b/src/Microsoft.AspNetCore.Razor.Language/Legacy/SpanEditHandler.cs
index 4bef77c6f7..dd9511ce70 100644
--- a/src/Microsoft.AspNetCore.Razor.Language/Legacy/SpanEditHandler.cs
+++ b/src/Microsoft.AspNetCore.Razor.Language/Legacy/SpanEditHandler.cs
@@ -38,14 +38,14 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy
public virtual EditResult ApplyChange(Span target, SourceChange change, bool force)
{
- var result = PartialParseResult.Accepted;
+ var result = PartialParseResultInternal.Accepted;
if (!force)
{
result = CanAcceptChange(target, change);
}
// If the change is accepted then apply the change
- if ((result & PartialParseResult.Accepted) == PartialParseResult.Accepted)
+ if ((result & PartialParseResultInternal.Accepted) == PartialParseResultInternal.Accepted)
{
return new EditResult(result, UpdateSpan(target, change));
}
@@ -60,9 +60,9 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy
(changeOldEnd < end || (changeOldEnd == end && AcceptedCharacters != AcceptedCharactersInternal.None));
}
- protected virtual PartialParseResult CanAcceptChange(Span target, SourceChange change)
+ protected virtual PartialParseResultInternal CanAcceptChange(Span target, SourceChange change)
{
- return PartialParseResult.Rejected;
+ return PartialParseResultInternal.Rejected;
}
protected virtual SpanBuilder UpdateSpan(Span target, SourceChange change)
diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/BackgroundParser.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/BackgroundParser.cs
new file mode 100644
index 0000000000..b71f4d8814
--- /dev/null
+++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/BackgroundParser.cs
@@ -0,0 +1,419 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// 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.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using Microsoft.AspNetCore.Razor.Language;
+using Microsoft.AspNetCore.Razor.Language.Legacy;
+using Microsoft.VisualStudio.Text;
+
+namespace Microsoft.VisualStudio.LanguageServices.Razor
+{
+ internal class BackgroundParser : IDisposable
+ {
+ private MainThreadState _main;
+ private BackgroundThread _bg;
+
+ public BackgroundParser(RazorTemplateEngine templateEngine, string filePath)
+ {
+ _main = new MainThreadState(filePath);
+ _bg = new BackgroundThread(_main, templateEngine, filePath);
+
+ _main.ResultsReady += (sender, args) => OnResultsReady(args);
+ }
+
+ ///
+ /// Fired on the main thread.
+ ///
+ public event EventHandler ResultsReady;
+
+ public bool IsIdle
+ {
+ get { return _main.IsIdle; }
+ }
+
+ public void Start()
+ {
+ _bg.Start();
+ }
+
+ public void Cancel()
+ {
+ _main.Cancel();
+ }
+
+ public void QueueChange(SourceChange change, ITextSnapshot snapshot)
+ {
+ var edit = new Edit(change, snapshot);
+ _main.QueueChange(edit);
+ }
+
+ public void Dispose()
+ {
+ _main.Cancel();
+ }
+
+ public IDisposable SynchronizeMainThreadState()
+ {
+ return _main.Lock();
+ }
+
+ protected virtual void OnResultsReady(DocumentParseCompleteEventArgs args)
+ {
+ var handler = ResultsReady;
+ if (handler != null)
+ {
+ handler(this, args);
+ }
+ }
+
+ private static bool TreesAreDifferent(RazorSyntaxTree leftTree, RazorSyntaxTree rightTree, IEnumerable edits, CancellationToken cancelToken)
+ {
+ return TreesAreDifferent(leftTree.Root, rightTree.Root, edits.Select(edit => edit.Change), cancelToken);
+ }
+
+ internal static bool TreesAreDifferent(Block leftTree, Block rightTree, IEnumerable changes, CancellationToken cancelToken)
+ {
+ // Apply all the pending changes to the original tree
+ // PERF: If this becomes a bottleneck, we can probably do it the other way around,
+ // i.e. visit the tree and find applicable changes for each node.
+ foreach (var change in changes)
+ {
+ cancelToken.ThrowIfCancellationRequested();
+
+ var changeOwner = leftTree.LocateOwner(change);
+
+ // Apply the change to the tree
+ if (changeOwner == null)
+ {
+ return true;
+ }
+
+ var result = changeOwner.EditHandler.ApplyChange(changeOwner, change, force: true);
+ changeOwner.ReplaceWith(result.EditedSpan);
+ }
+
+ // Now compare the trees
+ var treesDifferent = !leftTree.EquivalentTo(rightTree);
+ return treesDifferent;
+ }
+
+ private abstract class ThreadStateBase
+ {
+#if DEBUG
+ private int _id = -1;
+#endif
+ protected ThreadStateBase()
+ {
+ }
+
+ [Conditional("DEBUG")]
+ protected void SetThreadId(int id)
+ {
+#if DEBUG
+ _id = id;
+#endif
+ }
+
+ [Conditional("DEBUG")]
+ protected void EnsureOnThread()
+ {
+#if DEBUG
+ Debug.Assert(_id != -1, "SetThreadId was never called!");
+ Debug.Assert(Thread.CurrentThread.ManagedThreadId == _id, "Called from an unexpected thread!");
+#endif
+ }
+
+ [Conditional("DEBUG")]
+ protected void EnsureNotOnThread()
+ {
+#if DEBUG
+ Debug.Assert(_id != -1, "SetThreadId was never called!");
+ Debug.Assert(Thread.CurrentThread.ManagedThreadId != _id, "Called from an unexpected thread!");
+#endif
+ }
+ }
+
+ private class MainThreadState : ThreadStateBase, IDisposable
+ {
+ private readonly CancellationTokenSource _cancelSource = new CancellationTokenSource();
+ private readonly ManualResetEventSlim _hasParcel = new ManualResetEventSlim(false);
+ private CancellationTokenSource _currentParcelCancelSource;
+
+ private string _fileName;
+ private readonly object _stateLock = new object();
+ private IList _changes = new List();
+
+ public MainThreadState(string fileName)
+ {
+ _fileName = fileName;
+
+ SetThreadId(Thread.CurrentThread.ManagedThreadId);
+ }
+
+ public event EventHandler ResultsReady;
+
+ public CancellationToken CancelToken
+ {
+ get { return _cancelSource.Token; }
+ }
+
+ public bool IsIdle
+ {
+ get
+ {
+ lock (_stateLock)
+ {
+ return _currentParcelCancelSource == null;
+ }
+ }
+ }
+
+ public void Cancel()
+ {
+ EnsureOnThread();
+ _cancelSource.Cancel();
+ }
+
+ public IDisposable Lock()
+ {
+ Monitor.Enter(_stateLock);
+ return new DisposableAction(() => Monitor.Exit(_stateLock));
+ }
+
+ public void QueueChange(Edit edit)
+ {
+ EnsureOnThread();
+ lock (_stateLock)
+ {
+ // CurrentParcel token source is not null ==> There's a parse underway
+ if (_currentParcelCancelSource != null)
+ {
+ _currentParcelCancelSource.Cancel();
+ }
+
+ _changes.Add(edit);
+ _hasParcel.Set();
+ }
+ }
+
+ public WorkParcel GetParcel()
+ {
+ EnsureNotOnThread(); // Only the background thread can get a parcel
+ _hasParcel.Wait(_cancelSource.Token);
+ _hasParcel.Reset();
+ lock (_stateLock)
+ {
+ // Create a cancellation source for this parcel
+ _currentParcelCancelSource = new CancellationTokenSource();
+
+ var changes = _changes;
+ _changes = new List();
+ return new WorkParcel(changes, _currentParcelCancelSource.Token);
+ }
+ }
+
+ public void ReturnParcel(DocumentParseCompleteEventArgs args)
+ {
+ lock (_stateLock)
+ {
+ // Clear the current parcel cancellation source
+ if (_currentParcelCancelSource != null)
+ {
+ _currentParcelCancelSource.Dispose();
+ _currentParcelCancelSource = null;
+ }
+
+ // If there are things waiting to be parsed, just don't fire the event because we're already out of date
+ if (_changes.Any())
+ {
+ return;
+ }
+ }
+ var handler = ResultsReady;
+ if (handler != null)
+ {
+ handler(this, args);
+ }
+ }
+
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ protected virtual void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ if (_currentParcelCancelSource != null)
+ {
+ _currentParcelCancelSource.Dispose();
+ _currentParcelCancelSource = null;
+ }
+ _cancelSource.Dispose();
+ _hasParcel.Dispose();
+ }
+ }
+ }
+
+ private class BackgroundThread : ThreadStateBase
+ {
+ private MainThreadState _main;
+ private Thread _backgroundThread;
+ private CancellationToken _shutdownToken;
+ private RazorTemplateEngine _templateEngine;
+ private string _filePath;
+ private RazorSyntaxTree _currentSyntaxTree;
+ private IList _previouslyDiscarded = new List();
+
+ public BackgroundThread(MainThreadState main, RazorTemplateEngine templateEngine, string fileName)
+ {
+ // Run on MAIN thread!
+ _main = main;
+ _shutdownToken = _main.CancelToken;
+ _templateEngine = templateEngine;
+ _filePath = fileName;
+
+ _backgroundThread = new Thread(WorkerLoop);
+ SetThreadId(_backgroundThread.ManagedThreadId);
+ }
+
+ // **** ANY THREAD ****
+ public void Start()
+ {
+ _backgroundThread.Start();
+ }
+
+ // **** BACKGROUND THREAD ****
+ private void WorkerLoop()
+ {
+ var fileNameOnly = Path.GetFileName(_filePath);
+
+ try
+ {
+ EnsureOnThread();
+
+ while (!_shutdownToken.IsCancellationRequested)
+ {
+ // Grab the parcel of work to do
+ var parcel = _main.GetParcel();
+ if (parcel.Edits.Any())
+ {
+ try
+ {
+ DocumentParseCompleteEventArgs args = null;
+ using (var linkedCancel = CancellationTokenSource.CreateLinkedTokenSource(_shutdownToken, parcel.CancelToken))
+ {
+ if (!linkedCancel.IsCancellationRequested)
+ {
+ // Collect ALL changes
+ List allEdits;
+
+ if (_previouslyDiscarded != null)
+ {
+ allEdits = Enumerable.Concat(_previouslyDiscarded, parcel.Edits).ToList();
+ }
+ else
+ {
+ allEdits = parcel.Edits.ToList();
+ }
+
+ var finalEdit = allEdits.Last();
+
+ var results = ParseChange(finalEdit.Snapshot, linkedCancel.Token);
+
+ if (results != null && !linkedCancel.IsCancellationRequested)
+ {
+ // Clear discarded changes list
+ _previouslyDiscarded = null;
+
+ var treeStructureChanged = _currentSyntaxTree == null || TreesAreDifferent(_currentSyntaxTree, results.GetSyntaxTree(), allEdits, parcel.CancelToken);
+ _currentSyntaxTree = results.GetSyntaxTree();
+
+ // Build Arguments
+ args = new DocumentParseCompleteEventArgs(
+ finalEdit.Change,
+ finalEdit.Snapshot,
+ treeStructureChanged,
+ results);
+ }
+ else
+ {
+ // Parse completed but we were cancelled in the mean time. Add these to the discarded changes set
+ _previouslyDiscarded = allEdits;
+ }
+ }
+ }
+ if (args != null)
+ {
+ _main.ReturnParcel(args);
+ }
+ }
+ catch (OperationCanceledException)
+ {
+ }
+ }
+ else
+ {
+ Thread.Yield();
+ }
+ }
+ }
+ catch (OperationCanceledException)
+ {
+ // Do nothing. Just shut down.
+ }
+ finally
+ {
+ // Clean up main thread resources
+ _main.Dispose();
+ }
+ }
+
+ private RazorCodeDocument ParseChange(ITextSnapshot snapshot, CancellationToken token)
+ {
+ EnsureOnThread();
+
+ var sourceDocument = new TextSnapshotSourceDocument(snapshot, _filePath);
+ var imports = _templateEngine.GetImports(_filePath);
+
+ var codeDocument = RazorCodeDocument.Create(sourceDocument, imports);
+
+ _templateEngine.GenerateCode(codeDocument);
+ return codeDocument;
+ }
+ }
+
+ private class WorkParcel
+ {
+ public WorkParcel(IList changes, CancellationToken cancelToken)
+ {
+ Edits = changes;
+ CancelToken = cancelToken;
+ }
+
+ public CancellationToken CancelToken { get; }
+
+ public IList Edits { get; }
+ }
+
+ private class Edit
+ {
+ public Edit(SourceChange change, ITextSnapshot snapshot)
+ {
+ Change = change;
+ Snapshot = snapshot;
+ }
+
+ public SourceChange Change { get; }
+
+ public ITextSnapshot Snapshot { get; set; }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/DocumentParseCompleteEventArgs.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/DocumentParseCompleteEventArgs.cs
new file mode 100644
index 0000000000..c8dc5065c8
--- /dev/null
+++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/DocumentParseCompleteEventArgs.cs
@@ -0,0 +1,47 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using Microsoft.AspNetCore.Razor.Language;
+using Microsoft.VisualStudio.Text;
+
+namespace Microsoft.VisualStudio.LanguageServices.Razor
+{
+ ///
+ /// Arguments for the event in .
+ ///
+ public sealed class DocumentParseCompleteEventArgs : EventArgs
+ {
+ public DocumentParseCompleteEventArgs(
+ SourceChange change,
+ ITextSnapshot buffer,
+ bool treeStructureChanged,
+ RazorCodeDocument codeDocument)
+ {
+ SourceChange = change;
+ Buffer = buffer;
+ TreeStructureChanged = treeStructureChanged;
+ CodeDocument = codeDocument;
+ }
+
+ ///
+ /// The which triggered the re-parse.
+ ///
+ public SourceChange SourceChange { get; }
+
+ ///
+ /// The text snapshot used in the re-parse.
+ ///
+ public ITextSnapshot Buffer { get; }
+
+ ///
+ /// Indicates if the tree structure has actually changed since the previous re-parse.
+ ///
+ public bool TreeStructureChanged { get; }
+
+ ///
+ /// The result of the parsing and code generation.
+ ///
+ public RazorCodeDocument CodeDocument { get; }
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/PartialParseResult.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/PartialParseResult.cs
new file mode 100644
index 0000000000..2571342f69
--- /dev/null
+++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/PartialParseResult.cs
@@ -0,0 +1,21 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+
+namespace Microsoft.VisualStudio.LanguageServices.Razor
+{
+ [Flags]
+ public enum PartialParseResult
+ {
+ Rejected = 1,
+
+ Accepted = 2,
+
+ Provisional = 4,
+
+ SpanContextChanged = 8,
+
+ AutoCompleteBlock = 16
+ }
+}
diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/RazorEditorParser.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/RazorEditorParser.cs
new file mode 100644
index 0000000000..7de8648a74
--- /dev/null
+++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/RazorEditorParser.cs
@@ -0,0 +1,187 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Diagnostics;
+using Microsoft.AspNetCore.Razor.Language;
+using Microsoft.AspNetCore.Razor.Language.Legacy;
+using Microsoft.VisualStudio.Text;
+
+namespace Microsoft.VisualStudio.LanguageServices.Razor
+{
+ public class RazorEditorParser : IDisposable
+ {
+ private AspNetCore.Razor.Language.Legacy.Span _lastChangeOwner;
+ private AspNetCore.Razor.Language.Legacy.Span _lastAutoCompleteSpan;
+ private BackgroundParser _parser;
+
+ public RazorEditorParser(RazorTemplateEngine templateEngine, string filePath)
+ {
+ if (templateEngine == null)
+ {
+ throw new ArgumentNullException(nameof(templateEngine));
+ }
+
+ if (string.IsNullOrEmpty(filePath))
+ {
+ throw new ArgumentException(
+ AspNetCore.Razor.Language.Resources.ArgumentCannotBeNullOrEmpty,
+ nameof(filePath));
+ }
+
+ TemplateEngine = templateEngine;
+ _parser = new BackgroundParser(templateEngine, filePath);
+ _parser.ResultsReady += (sender, args) => OnDocumentParseComplete(args);
+ _parser.Start();
+ }
+
+ ///
+ /// Event fired when a full reparse of the document completes.
+ ///
+ public event EventHandler DocumentParseComplete;
+
+ public RazorTemplateEngine TemplateEngine { get; }
+
+ // Internal for testing.
+ internal RazorSyntaxTree CurrentSyntaxTree { get; private set; }
+
+ // Internal for testing.
+ internal bool LastResultProvisional { get; private set; }
+
+ public virtual string GetAutoCompleteString()
+ {
+ if (_lastAutoCompleteSpan?.EditHandler is AutoCompleteEditHandler editHandler)
+ {
+ return editHandler.AutoCompleteString;
+ }
+
+ return null;
+ }
+
+ public virtual PartialParseResult CheckForStructureChanges(SourceChange change, ITextSnapshot snapshot)
+ {
+ if (snapshot == null)
+ {
+ throw new ArgumentNullException(nameof(snapshot));
+ }
+
+ var result = PartialParseResultInternal.Rejected;
+
+ using (_parser.SynchronizeMainThreadState())
+ {
+ // Check if we can partial-parse
+ if (CurrentSyntaxTree != null && _parser.IsIdle)
+ {
+ result = TryPartialParse(change);
+ }
+ }
+
+ // If partial parsing failed or there were outstanding parser tasks, start a full reparse
+ if ((result & PartialParseResultInternal.Rejected) == PartialParseResultInternal.Rejected)
+ {
+ _parser.QueueChange(change, snapshot);
+ }
+
+ // Otherwise, remember if this was provisionally accepted for next partial parse
+ LastResultProvisional = (result & PartialParseResultInternal.Provisional) == PartialParseResultInternal.Provisional;
+ VerifyFlagsAreValid(result);
+
+ return (PartialParseResult)result;
+ }
+
+ ///
+ /// Disposes of this parser. Should be called when the editor window is closed and the document is unloaded.
+ ///
+ public void Dispose()
+ {
+ _parser.Dispose();
+ GC.SuppressFinalize(this);
+ }
+
+ private PartialParseResultInternal TryPartialParse(SourceChange change)
+ {
+ var result = PartialParseResultInternal.Rejected;
+
+ // Try the last change owner
+ if (_lastChangeOwner != null && _lastChangeOwner.EditHandler.OwnsChange(_lastChangeOwner, change))
+ {
+ var editResult = _lastChangeOwner.EditHandler.ApplyChange(_lastChangeOwner, change);
+ result = editResult.Result;
+ if ((editResult.Result & PartialParseResultInternal.Rejected) != PartialParseResultInternal.Rejected)
+ {
+ _lastChangeOwner.ReplaceWith(editResult.EditedSpan);
+ }
+
+ return result;
+ }
+
+ // Locate the span responsible for this change
+ _lastChangeOwner = CurrentSyntaxTree.Root.LocateOwner(change);
+
+ if (LastResultProvisional)
+ {
+ // Last change owner couldn't accept this, so we must do a full reparse
+ result = PartialParseResultInternal.Rejected;
+ }
+ else if (_lastChangeOwner != null)
+ {
+ var editResult = _lastChangeOwner.EditHandler.ApplyChange(_lastChangeOwner, change);
+ result = editResult.Result;
+ if ((editResult.Result & PartialParseResultInternal.Rejected) != PartialParseResultInternal.Rejected)
+ {
+ _lastChangeOwner.ReplaceWith(editResult.EditedSpan);
+ }
+ if ((result & PartialParseResultInternal.AutoCompleteBlock) == PartialParseResultInternal.AutoCompleteBlock)
+ {
+ _lastAutoCompleteSpan = _lastChangeOwner;
+ }
+ else
+ {
+ _lastAutoCompleteSpan = null;
+ }
+ }
+
+ return result;
+ }
+
+ private void OnDocumentParseComplete(DocumentParseCompleteEventArgs args)
+ {
+ using (_parser.SynchronizeMainThreadState())
+ {
+ CurrentSyntaxTree = args.CodeDocument.GetSyntaxTree();
+ _lastChangeOwner = null;
+ }
+
+ Debug.Assert(args != null, "Event arguments cannot be null");
+ EventHandler handler = DocumentParseComplete;
+ if (handler != null)
+ {
+ try
+ {
+ handler(this, args);
+ }
+ catch (Exception ex)
+ {
+ Debug.WriteLine("[RzEd] Document Parse Complete Handler Threw: " + ex.ToString());
+ }
+ }
+ }
+
+ [Conditional("DEBUG")]
+ private static void VerifyFlagsAreValid(PartialParseResultInternal result)
+ {
+ Debug.Assert(((result & PartialParseResultInternal.Accepted) == PartialParseResultInternal.Accepted) ||
+ ((result & PartialParseResultInternal.Rejected) == PartialParseResultInternal.Rejected),
+ "Partial Parse result does not have either of Accepted or Rejected flags set");
+ Debug.Assert(((result & PartialParseResultInternal.Rejected) == PartialParseResultInternal.Rejected) ||
+ ((result & PartialParseResultInternal.SpanContextChanged) != PartialParseResultInternal.SpanContextChanged),
+ "Partial Parse result was Accepted AND had SpanContextChanged flag set");
+ Debug.Assert(((result & PartialParseResultInternal.Rejected) == PartialParseResultInternal.Rejected) ||
+ ((result & PartialParseResultInternal.AutoCompleteBlock) != PartialParseResultInternal.AutoCompleteBlock),
+ "Partial Parse result was Accepted AND had AutoCompleteBlock flag set");
+ Debug.Assert(((result & PartialParseResultInternal.Accepted) == PartialParseResultInternal.Accepted) ||
+ ((result & PartialParseResultInternal.Provisional) != PartialParseResultInternal.Provisional),
+ "Partial Parse result was Rejected AND had Provisional flag set");
+ }
+ }
+}
diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/TextSnapshotSourceDocument.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/TextSnapshotSourceDocument.cs
new file mode 100644
index 0000000000..37bb73f106
--- /dev/null
+++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/TextSnapshotSourceDocument.cs
@@ -0,0 +1,79 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Text;
+using Microsoft.AspNetCore.Razor.Language;
+using Microsoft.VisualStudio.Text;
+
+namespace Microsoft.VisualStudio.LanguageServices.Razor
+{
+ internal class TextSnapshotSourceDocument : RazorSourceDocument
+ {
+ private readonly ITextSnapshot _buffer;
+ private readonly RazorSourceLineCollection _lines;
+
+ public TextSnapshotSourceDocument(ITextSnapshot snapshot, string filePath)
+ {
+ if (snapshot == null)
+ {
+ throw new ArgumentNullException(nameof(snapshot));
+ }
+
+ if (filePath == null)
+ {
+ throw new ArgumentNullException(nameof(filePath));
+ }
+
+ _buffer = snapshot;
+ FilePath = filePath;
+
+ _lines = new DefaultRazorSourceLineCollection(this);
+ }
+
+ public override char this[int position] => _buffer[position];
+
+ public override Encoding Encoding => Encoding.UTF8;
+
+ public override int Length => _buffer.Length;
+
+ public override RazorSourceLineCollection Lines => _lines;
+
+ public override string FilePath { get; }
+
+ public override void CopyTo(int sourceIndex, char[] destination, int destinationIndex, int count)
+ {
+ if (destination == null)
+ {
+ throw new ArgumentNullException(nameof(destination));
+ }
+
+ if (sourceIndex < 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(sourceIndex));
+ }
+
+ if (destinationIndex < 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(destinationIndex));
+ }
+
+ if (count < 0 || count > Length - sourceIndex || count > destination.Length - destinationIndex)
+ {
+ throw new ArgumentOutOfRangeException(nameof(count));
+ }
+
+ if (count == 0)
+ {
+ return;
+ }
+
+ for (var i = 0; i < count; i++)
+ {
+ destination[destinationIndex + i] = this[sourceIndex + i];
+ }
+ }
+
+ public override byte[] GetChecksum() => throw new NotImplementedException();
+ }
+}
\ No newline at end of file
diff --git a/test/Microsoft.AspNetCore.Razor.Language.Test/DirectiveTokenEditHandlerTest.cs b/test/Microsoft.AspNetCore.Razor.Language.Test/DirectiveTokenEditHandlerTest.cs
index 1a5aae94b7..0ddfa731e4 100644
--- a/test/Microsoft.AspNetCore.Razor.Language.Test/DirectiveTokenEditHandlerTest.cs
+++ b/test/Microsoft.AspNetCore.Razor.Language.Test/DirectiveTokenEditHandlerTest.cs
@@ -29,7 +29,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Test
var result = directiveTokenHandler.CanAcceptChange(target, sourceChange);
// Assert
- Assert.Equal(PartialParseResult.Accepted | PartialParseResult.Provisional, result);
+ Assert.Equal(PartialParseResultInternal.Accepted | PartialParseResultInternal.Provisional, result);
}
[Theory]
@@ -50,7 +50,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Test
var result = directiveTokenHandler.CanAcceptChange(target, sourceChange);
// Assert
- Assert.Equal(PartialParseResult.Rejected, result);
+ Assert.Equal(PartialParseResultInternal.Rejected, result);
}
private class TestDirectiveTokenEditHandler : DirectiveTokenEditHandler
@@ -59,7 +59,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Test
{
}
- public new PartialParseResult CanAcceptChange(Span target, SourceChange change)
+ public new PartialParseResultInternal CanAcceptChange(Span target, SourceChange change)
=> base.CanAcceptChange(target, change);
}
}
diff --git a/test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/StringTextBuffer.cs b/test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/StringTextBuffer.cs
deleted file mode 100644
index 3f5a94a859..0000000000
--- a/test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/StringTextBuffer.cs
+++ /dev/null
@@ -1,53 +0,0 @@
-// Copyright (c) .NET Foundation. All rights reserved.
-// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
-
-using System;
-
-namespace Microsoft.AspNetCore.Razor.Language.Legacy
-{
- public class StringTextBuffer : ITextBuffer, IDisposable
- {
- private string _buffer;
- public bool Disposed { get; set; }
-
- public StringTextBuffer(string buffer)
- {
- _buffer = buffer;
- }
-
- public int Length
- {
- get { return _buffer.Length; }
- }
-
- public int Position { get; set; }
-
- public int Read()
- {
- if (Position >= _buffer.Length)
- {
- return -1;
- }
- return _buffer[Position++];
- }
-
- public int Peek()
- {
- if (Position >= _buffer.Length)
- {
- return -1;
- }
- return _buffer[Position];
- }
-
- public void Dispose()
- {
- Disposed = true;
- }
-
- public object VersionToken
- {
- get { return _buffer; }
- }
- }
-}
diff --git a/test/Microsoft.AspNetCore.Razor.Language.Test/TestFiles/DesignTime/Simple.cshtml b/test/Microsoft.AspNetCore.Razor.Language.Test/TestFiles/DesignTime/Simple.cshtml
deleted file mode 100644
index b50db22f77..0000000000
--- a/test/Microsoft.AspNetCore.Razor.Language.Test/TestFiles/DesignTime/Simple.cshtml
+++ /dev/null
@@ -1,16 +0,0 @@
-@{
- string hello = "Hello, World";
-}
-
-
-
- Simple Page
-
-
- Simple Page
- @hello
-
- @foreach(char c in hello) {@c}
-
-
-
\ No newline at end of file
diff --git a/test/Microsoft.AspNetCore.Razor.Language.Test/TestFiles/DesignTime/Simple.txt b/test/Microsoft.AspNetCore.Razor.Language.Test/TestFiles/DesignTime/Simple.txt
deleted file mode 100644
index 69d42ccf0e..0000000000
--- a/test/Microsoft.AspNetCore.Razor.Language.Test/TestFiles/DesignTime/Simple.txt
+++ /dev/null
@@ -1,43 +0,0 @@
-namespace Razor
-{
- #line hidden
- public class Template
- {
- #pragma warning disable 219
- private void __RazorDirectiveTokenHelpers__() {
- }
- #pragma warning restore 219
- private static System.Object __o = null;
- #pragma warning disable 1998
- public async override global::System.Threading.Tasks.Task ExecuteAsync()
- {
-#line 1 "C:\This\Path\Is\Just\For\Line\Pragmas.cshtml"
-
- string hello = "Hello, World";
-
-#line default
-#line hidden
-#line 11 "C:\This\Path\Is\Just\For\Line\Pragmas.cshtml"
- __o = hello;
-
-#line default
-#line hidden
-#line 13 "C:\This\Path\Is\Just\For\Line\Pragmas.cshtml"
- foreach(char c in hello) {
-
-#line default
-#line hidden
-#line 13 "C:\This\Path\Is\Just\For\Line\Pragmas.cshtml"
- __o = c;
-
-#line default
-#line hidden
-#line 13 "C:\This\Path\Is\Just\For\Line\Pragmas.cshtml"
- }
-
-#line default
-#line hidden
- }
- #pragma warning restore 1998
- }
-}
diff --git a/test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/BlockExtensions.cs b/test/Microsoft.AspNetCore.Razor.Test.Common/Language/Legacy/BlockExtensions.cs
similarity index 100%
rename from test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/BlockExtensions.cs
rename to test/Microsoft.AspNetCore.Razor.Test.Common/Language/Legacy/BlockExtensions.cs
diff --git a/test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/BlockFactory.cs b/test/Microsoft.AspNetCore.Razor.Test.Common/Language/Legacy/BlockFactory.cs
similarity index 100%
rename from test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/BlockFactory.cs
rename to test/Microsoft.AspNetCore.Razor.Test.Common/Language/Legacy/BlockFactory.cs
diff --git a/test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/BlockTypes.cs b/test/Microsoft.AspNetCore.Razor.Test.Common/Language/Legacy/BlockTypes.cs
similarity index 100%
rename from test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/BlockTypes.cs
rename to test/Microsoft.AspNetCore.Razor.Test.Common/Language/Legacy/BlockTypes.cs
diff --git a/test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/ErrorCollector.cs b/test/Microsoft.AspNetCore.Razor.Test.Common/Language/Legacy/ErrorCollector.cs
similarity index 100%
rename from test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/ErrorCollector.cs
rename to test/Microsoft.AspNetCore.Razor.Test.Common/Language/Legacy/ErrorCollector.cs
diff --git a/test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/ParserTestBase.cs b/test/Microsoft.AspNetCore.Razor.Test.Common/Language/Legacy/ParserTestBase.cs
similarity index 100%
rename from test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/ParserTestBase.cs
rename to test/Microsoft.AspNetCore.Razor.Test.Common/Language/Legacy/ParserTestBase.cs
diff --git a/test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/RawTextSymbol.cs b/test/Microsoft.AspNetCore.Razor.Test.Common/Language/Legacy/RawTextSymbol.cs
similarity index 100%
rename from test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/RawTextSymbol.cs
rename to test/Microsoft.AspNetCore.Razor.Test.Common/Language/Legacy/RawTextSymbol.cs
diff --git a/test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/TestSpanBuilder.cs b/test/Microsoft.AspNetCore.Razor.Test.Common/Language/Legacy/TestSpanBuilder.cs
similarity index 100%
rename from test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/TestSpanBuilder.cs
rename to test/Microsoft.AspNetCore.Razor.Test.Common/Language/Legacy/TestSpanBuilder.cs
diff --git a/test/Microsoft.AspNetCore.Razor.Language.Test/SyntaxTreeVerifier.cs b/test/Microsoft.AspNetCore.Razor.Test.Common/Language/SyntaxTreeVerifier.cs
similarity index 100%
rename from test/Microsoft.AspNetCore.Razor.Language.Test/SyntaxTreeVerifier.cs
rename to test/Microsoft.AspNetCore.Razor.Test.Common/Language/SyntaxTreeVerifier.cs
diff --git a/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Microsoft.VisualStudio.LanguageServices.Razor.Test.csproj b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Microsoft.VisualStudio.LanguageServices.Razor.Test.csproj
index 96bf0959fb..1b1eb040d1 100644
--- a/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Microsoft.VisualStudio.LanguageServices.Razor.Test.csproj
+++ b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Microsoft.VisualStudio.LanguageServices.Razor.Test.csproj
@@ -20,6 +20,7 @@
+
diff --git a/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/RazorEditorParserTest.cs b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/RazorEditorParserTest.cs
new file mode 100644
index 0000000000..5fd369d42a
--- /dev/null
+++ b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/RazorEditorParserTest.cs
@@ -0,0 +1,1479 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// 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.Diagnostics;
+using System.Threading;
+using Microsoft.AspNetCore.Mvc.Razor.Extensions;
+using Microsoft.AspNetCore.Razor.Language;
+using Microsoft.AspNetCore.Razor.Language.Legacy;
+using Microsoft.VisualStudio.Text;
+using Xunit;
+
+namespace Microsoft.VisualStudio.LanguageServices.Razor
+{
+ public class RazorEditorParserTest
+ {
+ private static readonly TestFile SimpleCSHTMLDocument = TestFile.Create("TestFiles/DesignTime/Simple.cshtml", typeof(RazorEditorParserTest));
+ private static readonly TestFile SimpleCSHTMLDocumentGenerated = TestFile.Create("TestFiles/DesignTime/Simple.txt", typeof(RazorEditorParserTest));
+ private const string TestLinePragmaFileName = "C:\\This\\Path\\Is\\Just\\For\\Line\\Pragmas.cshtml";
+
+ public static TheoryData TagHelperPartialParseRejectData
+ {
+ get
+ {
+ // change, (Block)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,
+ attributeStructure: AttributeStructure.Minimized)
+ }))
+ },
+ {
+ CreateInsertionChange("", 12, "ibute"),
+ new MarkupBlock(
+ new MarkupTagHelperBlock(
+ "p",
+ attributes: new List
+ {
+ new TagHelperAttributeNode(
+ "some-attribute",
+ value: null,
+ attributeStructure: AttributeStructure.Minimized)
+ }))
+ },
+ {
+ CreateInsertionChange("", 2, " before"),
+ new MarkupBlock(
+ new MarkupTagHelperBlock(
+ "p",
+ attributes: new List
+ {
+ new TagHelperAttributeNode(
+ "before",
+ value: null,
+ attributeStructure: AttributeStructure.Minimized),
+ new TagHelperAttributeNode(
+ "some-attr",
+ value: null,
+ attributeStructure: AttributeStructure.Minimized)
+ }))
+ },
+ };
+ }
+ }
+
+ [Theory]
+ [MemberData(nameof(TagHelperPartialParseRejectData))]
+ public void TagHelperTagBodiesRejectPartialChanges(object editObject, object expectedDocument)
+ {
+ // Arrange
+ var edit = (TestEdit)editObject;
+ var builder = TagHelperDescriptorBuilder.Create("PTagHelper", "TestAssembly");
+ builder.SetTypeName("PTagHelper");
+ builder.TagMatchingRule(rule => rule.TagName = "p");
+ var descriptors = new[]
+ {
+ builder.Build()
+ };
+
+ var parser = new RazorEditorParser(CreateTemplateEngine(@"C:\This\Is\A\Test\Path"), @"C:\This\Is\A\Test\Path");
+
+ using (var manager = new TestParserManager(parser))
+ {
+ manager.InitializeWithDocument(edit.OldSnapshot);
+
+ // Act
+ var result = manager.CheckForStructureChangesAndWait(edit);
+
+ // Assert
+ Assert.Equal(PartialParseResult.Rejected, result);
+ Assert.Equal(2, manager.ParseCount);
+ }
+ }
+
+ public static TheoryData TagHelperAttributeAcceptData
+ {
+ get
+ {
+ var factory = new SpanFactory();
+
+ // change, (Block)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(AcceptedCharactersInternal.NonWhiteSpace)))),
+ AttributeStructure.SingleQuotes)
+ })),
+ PartialParseResult.Accepted | PartialParseResult.Provisional
+ },
+ {
+ CreateInsertionChange("", 21, "."),
+ new MarkupBlock(
+ new MarkupTagHelperBlock(
+ "p",
+ attributes: new List
+ {
+ new TagHelperAttributeNode(
+ "obj-attr",
+ factory.CodeMarkup("DateTime."),
+ AttributeStructure.SingleQuotes)
+ })),
+ PartialParseResult.Accepted
+ },
+ {
+ CreateInsertionChange("", 25, "."),
+ new MarkupBlock(
+ new MarkupTagHelperBlock(
+ "p",
+ attributes: new List
+ {
+ new TagHelperAttributeNode(
+ "obj-attr",
+ factory.CodeMarkup("1 + DateTime."),
+ AttributeStructure.SingleQuotes)
+ })),
+ PartialParseResult.Accepted
+ },
+ {
+ CreateInsertionChange("", 34, "."),
+ new MarkupBlock(
+ new MarkupTagHelperBlock(
+ "p",
+ attributes: new List
+ {
+ new TagHelperAttributeNode(
+ "before-attr",
+ value: null,
+ attributeStructure: AttributeStructure.Minimized),
+ new TagHelperAttributeNode(
+ "str-attr",
+ new MarkupBlock(
+ new MarkupBlock(
+ new ExpressionBlock(
+ factory.CodeTransition(),
+ factory
+ .Code("DateTime.")
+ .AsImplicitExpression(CSharpCodeParser.DefaultKeywords)
+ .Accepts(AcceptedCharactersInternal.NonWhiteSpace)))),
+ AttributeStructure.SingleQuotes),
+ new TagHelperAttributeNode(
+ "after-attr",
+ value: null,
+ attributeStructure: AttributeStructure.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(AcceptedCharactersInternal.NonWhiteSpace))),
+ factory.Markup(" after")),
+ AttributeStructure.SingleQuotes)
+ })),
+ PartialParseResult.Accepted | PartialParseResult.Provisional
+ },
+ };
+ }
+ }
+
+ [Theory]
+ [MemberData(nameof(TagHelperAttributeAcceptData))]
+ public void TagHelperAttributesAreLocatedAndAcceptChangesCorrectly(
+ object editObject,
+ object expectedDocument,
+ PartialParseResult partialParseResult)
+ {
+ // Arrange
+ var edit = (TestEdit)editObject;
+ var builder = TagHelperDescriptorBuilder.Create("PTagHelper", "Test");
+ builder.SetTypeName("PTagHelper");
+ builder.TagMatchingRule(rule => rule.TagName = "p");
+ builder.BindAttribute(attribute =>
+ {
+ attribute.Name = "obj-attr";
+ attribute.TypeName = typeof(object).FullName;
+ attribute.SetPropertyName("ObjectAttribute");
+ });
+ builder.BindAttribute(attribute =>
+ {
+ attribute.Name = "str-attr";
+ attribute.TypeName = typeof(string).FullName;
+ attribute.SetPropertyName("StringAttribute");
+ });
+ var descriptors = new[] { builder.Build() };
+
+ var parser = new RazorEditorParser(CreateTemplateEngine(@"C:\This\Is\A\Test\Path", descriptors), @"C:\This\Is\A\Test\Path");
+
+ using (var manager = new TestParserManager(parser))
+ {
+ manager.InitializeWithDocument(edit.OldSnapshot);
+
+ // Act
+ var result = manager.CheckForStructureChangesAndWait(edit);
+
+ // Assert
+ Assert.Equal(partialParseResult, result);
+ Assert.Equal(1, manager.ParseCount);
+ }
+ }
+
+ [Fact]
+ public void ConstructorRequiresNonNullPhysicalPath()
+ {
+ Assert.Throws("filePath", () => new RazorEditorParser(CreateTemplateEngine(), null));
+ }
+
+ [Fact]
+ public void ConstructorRequiresNonEmptyPhysicalPath()
+ {
+ Assert.Throws("filePath", () => new RazorEditorParser(CreateTemplateEngine(), string.Empty));
+ }
+
+ [Theory]
+ [InlineData(" ")]
+ [InlineData("\r\n")]
+ [InlineData("abcdefg")]
+ [InlineData("\f\r\n abcd \t")]
+ public void TreesAreDifferentReturnsFalseForAddedContent(string content)
+ {
+ // Arrange
+ var factory = new SpanFactory();
+ var blockFactory = new BlockFactory(factory);
+ var original = new MarkupBlock(
+ blockFactory.MarkupTagBlock(""),
+ blockFactory.TagHelperBlock(
+ tagName: "div",
+ tagMode: TagMode.StartTagAndEndTag,
+ start: new SourceLocation(3, 0, 3),
+ startTag: blockFactory.MarkupTagBlock("
"),
+ children: new SyntaxTreeNode[]
+ {
+ factory.Markup($"{Environment.NewLine}{Environment.NewLine}")
+ },
+ endTag: blockFactory.MarkupTagBlock("
")),
+ blockFactory.MarkupTagBlock("
"));
+
+ factory.Reset();
+
+ var modified = new MarkupBlock(
+ blockFactory.MarkupTagBlock(""),
+ blockFactory.TagHelperBlock(
+ tagName: "div",
+ tagMode: TagMode.StartTagAndEndTag,
+ start: new SourceLocation(3, 0, 3),
+ startTag: blockFactory.MarkupTagBlock("
"),
+ children: new SyntaxTreeNode[]
+ {
+ factory.Markup($"{Environment.NewLine}{content}{Environment.NewLine}")
+ },
+ endTag: blockFactory.MarkupTagBlock("
")),
+ blockFactory.MarkupTagBlock(""));
+ original.LinkNodes();
+ modified.LinkNodes();
+
+ // Act
+ var treesAreDifferent = BackgroundParser.TreesAreDifferent(
+ original,
+ modified,
+ new[]
+ {
+ new SourceChange(
+ absoluteIndex: 8 + Environment.NewLine.Length,
+ length: 0,
+ newText: content)
+ },
+ CancellationToken.None);
+
+ // Assert
+ Assert.False(treesAreDifferent);
+ }
+
+ [Fact]
+ public void TreesAreDifferentReturnsTrueIfTreeStructureIsDifferent()
+ {
+ var factory = new SpanFactory();
+ var original = new MarkupBlock(
+ factory.Markup(""),
+ new ExpressionBlock(
+ factory.CodeTransition()),
+ factory.Markup("
"));
+ var modified = new MarkupBlock(
+ factory.Markup(""),
+ new ExpressionBlock(
+ factory.CodeTransition("@"),
+ factory.Code("f")
+ .AsImplicitExpression(CSharpCodeParser.DefaultKeywords, acceptTrailingDot: false)),
+ factory.Markup("
"));
+ Assert.True(BackgroundParser.TreesAreDifferent(
+ original,
+ modified,
+ new[]
+ {
+ new SourceChange(absoluteIndex: 4, length: 0, newText: "f")
+ },
+ CancellationToken.None));
+ }
+
+ [Fact]
+ public void TreesAreDifferentReturnsFalseIfTreeStructureIsSame()
+ {
+ var factory = new SpanFactory();
+ var original = new MarkupBlock(
+ factory.Markup(""),
+ new ExpressionBlock(
+ factory.CodeTransition(),
+ factory.Code("f")
+ .AsImplicitExpression(CSharpCodeParser.DefaultKeywords, acceptTrailingDot: false)),
+ factory.Markup("
"));
+ factory.Reset();
+ var modified = new MarkupBlock(
+ factory.Markup(""),
+ new ExpressionBlock(
+ factory.CodeTransition(),
+ factory.Code("foo")
+ .AsImplicitExpression(CSharpCodeParser.DefaultKeywords, acceptTrailingDot: false)),
+ factory.Markup("
"));
+ original.LinkNodes();
+ modified.LinkNodes();
+ Assert.False(BackgroundParser.TreesAreDifferent(
+ original,
+ modified,
+ new[]
+ {
+ new SourceChange(absoluteIndex: 5, length: 0, newText: "oo")
+ },
+ CancellationToken.None));
+ }
+
+ [Fact]
+ public void CheckForStructureChangesStartsFullReparseIfChangeOverlapsMultipleSpans()
+ {
+ // Arrange
+ using (var parser = new RazorEditorParser(CreateTemplateEngine(), TestLinePragmaFileName))
+ {
+ var original = new StringTextSnapshot("Foo @bar Baz");
+ var changed = new StringTextSnapshot("Foo @bap Daz");
+ var change = new SourceChange(7, 3, "p D");
+
+ var parseComplete = new ManualResetEventSlim();
+ var parseCount = 0;
+ parser.DocumentParseComplete += (sender, args) =>
+ {
+ Interlocked.Increment(ref parseCount);
+ parseComplete.Set();
+ };
+
+ Assert.Equal(PartialParseResult.Rejected, parser.CheckForStructureChanges(change, original));
+ DoWithTimeoutIfNotDebugging(parseComplete.Wait); // Wait for the parse to finish
+ parseComplete.Reset();
+
+ // Act
+ var result = parser.CheckForStructureChanges(change, original);
+
+ // Assert
+ Assert.Equal(PartialParseResult.Rejected, result);
+ DoWithTimeoutIfNotDebugging(parseComplete.Wait);
+ Assert.Equal(2, parseCount);
+ }
+ }
+
+ [Fact]
+ public void AwaitPeriodInsertionAcceptedProvisionally()
+ {
+ // Arrange
+ var factory = new SpanFactory();
+ var changed = new StringTextSnapshot("foo @await Html. baz");
+ var old = new StringTextSnapshot("foo @await Html baz");
+
+ // Act and Assert
+ RunPartialParseTest(new TestEdit(15, 0, old, 1, changed, "."),
+ new MarkupBlock(
+ factory.Markup("foo "),
+ new ExpressionBlock(
+ factory.CodeTransition(),
+ factory.Code("await Html.").AsImplicitExpression(CSharpCodeParser.DefaultKeywords).Accepts(AcceptedCharactersInternal.WhiteSpace | AcceptedCharactersInternal.NonWhiteSpace)),
+ factory.Markup(" baz")), additionalFlags: PartialParseResult.Provisional);
+ }
+
+ [Fact]
+ public void ImplicitExpressionAcceptsInnerInsertionsInStatementBlock()
+ {
+ // Arrange
+ var factory = new SpanFactory();
+ var changed = new StringTextSnapshot("@{" + Environment.NewLine
+ + " @DateTime..Now" + Environment.NewLine
+ + "}");
+ var old = new StringTextSnapshot("@{" + Environment.NewLine
+ + " @DateTime.Now" + Environment.NewLine
+ + "}");
+
+ // Act and Assert
+ RunPartialParseTest(new TestEdit(17, 0, old, 1, changed, "."),
+ new MarkupBlock(
+ factory.EmptyHtml(),
+ new StatementBlock(
+ factory.CodeTransition(),
+ factory.MetaCode("{").Accepts(AcceptedCharactersInternal.None),
+ factory.Code(Environment.NewLine + " ")
+ .AsStatement()
+ .AutoCompleteWith(autoCompleteString: null),
+ new ExpressionBlock(
+ factory.CodeTransition(),
+ factory.Code("DateTime..Now")
+ .AsImplicitExpression(CSharpCodeParser.DefaultKeywords, acceptTrailingDot: true)
+ .Accepts(AcceptedCharactersInternal.NonWhiteSpace)),
+ factory.Code(Environment.NewLine).AsStatement(),
+ factory.MetaCode("}").Accepts(AcceptedCharactersInternal.None)),
+ factory.EmptyHtml()));
+ }
+
+ [Fact]
+ public void ImplicitExpressionAcceptsInnerInsertions()
+ {
+ // Arrange
+ var factory = new SpanFactory();
+ var changed = new StringTextSnapshot("foo @DateTime..Now baz");
+ var old = new StringTextSnapshot("foo @DateTime.Now baz");
+
+ // Act and Assert
+ RunPartialParseTest(new TestEdit(13, 0, old, 1, changed, "."),
+ new MarkupBlock(
+ factory.Markup("foo "),
+ new ExpressionBlock(
+ factory.CodeTransition(),
+ factory.Code("DateTime..Now").AsImplicitExpression(CSharpCodeParser.DefaultKeywords).Accepts(AcceptedCharactersInternal.NonWhiteSpace)),
+ factory.Markup(" baz")), additionalFlags: PartialParseResult.Provisional);
+ }
+
+ [Fact]
+ public void ImplicitExpressionAcceptsWholeIdentifierReplacement()
+ {
+ // Arrange
+ var factory = new SpanFactory();
+ var old = new StringTextSnapshot("foo @date baz");
+ var changed = new StringTextSnapshot("foo @DateTime baz");
+
+ // Act and Assert
+ RunPartialParseTest(new TestEdit(5, 4, old, 8, changed, "DateTime"),
+ new MarkupBlock(
+ factory.Markup("foo "),
+ new ExpressionBlock(
+ factory.CodeTransition(),
+ factory.Code("DateTime").AsImplicitExpression(CSharpCodeParser.DefaultKeywords).Accepts(AcceptedCharactersInternal.NonWhiteSpace)),
+ factory.Markup(" baz")));
+ }
+
+ [Fact]
+ public void ImplicitExpressionRejectsWholeIdentifierReplacementToKeyword()
+ {
+ // Arrange
+ var parser = new RazorEditorParser(CreateTemplateEngine(@"C:\This\Is\A\Test\Path"), @"C:\This\Is\A\Test\Path");
+
+ using (var manager = new TestParserManager(parser))
+ {
+ var old = new StringTextSnapshot("foo @date baz");
+ var changed = new StringTextSnapshot("foo @if baz");
+ var edit = new TestEdit(5, 4, old, 2, changed, "if");
+ manager.InitializeWithDocument(old);
+
+ // Act
+ var result = manager.CheckForStructureChangesAndWait(edit);
+
+ // Assert
+ Assert.Equal(PartialParseResult.Rejected, result);
+ Assert.Equal(2, manager.ParseCount);
+ }
+ }
+
+ [Fact]
+ public void ImplicitExpressionRejectsWholeIdentifierReplacementToDirective()
+ {
+ // Arrange
+ var parser = new RazorEditorParser(CreateTemplateEngine(@"C:\This\Is\A\Test\Path"), @"C:\This\Is\A\Test\Path");
+
+ using (var manager = new TestParserManager(parser))
+ {
+ var old = new StringTextSnapshot("foo @date baz");
+ var changed = new StringTextSnapshot("foo @inherits baz");
+ var SourceChange = new TestEdit(5, 4, old, 8, changed, "inherits");
+ manager.InitializeWithDocument(old);
+
+ // Act
+ var result = manager.CheckForStructureChangesAndWait(SourceChange);
+
+ // Assert
+ Assert.Equal(PartialParseResult.Rejected | PartialParseResult.SpanContextChanged, result);
+ Assert.Equal(2, manager.ParseCount);
+ }
+ }
+
+ [Fact]
+ public void ImplicitExpressionAcceptsPrefixIdentifierReplacements_SingleSymbol()
+ {
+ // Arrange
+ var factory = new SpanFactory();
+ var old = new StringTextSnapshot("foo @dTime baz");
+ var changed = new StringTextSnapshot("foo @DateTime baz");
+
+ // Act and Assert
+ RunPartialParseTest(new TestEdit(5, 1, old, 4, changed, "Date"),
+ new MarkupBlock(
+ factory.Markup("foo "),
+ new ExpressionBlock(
+ factory.CodeTransition(),
+ factory.Code("DateTime").AsImplicitExpression(CSharpCodeParser.DefaultKeywords).Accepts(AcceptedCharactersInternal.NonWhiteSpace)),
+ factory.Markup(" baz")));
+ }
+
+ [Fact]
+ public void ImplicitExpressionAcceptsPrefixIdentifierReplacements_MultipleSymbols()
+ {
+ // Arrange
+ var factory = new SpanFactory();
+ var old = new StringTextSnapshot("foo @dTime.Now baz");
+ var changed = new StringTextSnapshot("foo @DateTime.Now baz");
+
+ // Act and Assert
+ RunPartialParseTest(new TestEdit(5, 1, old, 4, changed, "Date"),
+ new MarkupBlock(
+ factory.Markup("foo "),
+ new ExpressionBlock(
+ factory.CodeTransition(),
+ factory.Code("DateTime.Now").AsImplicitExpression(CSharpCodeParser.DefaultKeywords).Accepts(AcceptedCharactersInternal.NonWhiteSpace)),
+ factory.Markup(" baz")));
+ }
+
+ [Fact]
+ public void ImplicitExpressionAcceptsSuffixIdentifierReplacements_SingleSymbol()
+ {
+ // Arrange
+ var factory = new SpanFactory();
+ var old = new StringTextSnapshot("foo @Datet baz");
+ var changed = new StringTextSnapshot("foo @DateTime baz");
+
+ // Act and Assert
+ RunPartialParseTest(new TestEdit(9, 1, old, 4, changed, "Time"),
+ new MarkupBlock(
+ factory.Markup("foo "),
+ new ExpressionBlock(
+ factory.CodeTransition(),
+ factory.Code("DateTime").AsImplicitExpression(CSharpCodeParser.DefaultKeywords).Accepts(AcceptedCharactersInternal.NonWhiteSpace)),
+ factory.Markup(" baz")));
+ }
+
+ [Fact]
+ public void ImplicitExpressionAcceptsSuffixIdentifierReplacements_MultipleSymbols()
+ {
+ // Arrange
+ var factory = new SpanFactory();
+ var old = new StringTextSnapshot("foo @DateTime.n baz");
+ var changed = new StringTextSnapshot("foo @DateTime.Now baz");
+
+ // Act and Assert
+ RunPartialParseTest(new TestEdit(14, 1, old, 3, changed, "Now"),
+ new MarkupBlock(
+ factory.Markup("foo "),
+ new ExpressionBlock(
+ factory.CodeTransition(),
+ factory.Code("DateTime.Now").AsImplicitExpression(CSharpCodeParser.DefaultKeywords).Accepts(AcceptedCharactersInternal.NonWhiteSpace)),
+ factory.Markup(" baz")));
+ }
+
+ [Fact]
+ public void ImplicitExpressionAcceptsSurroundedIdentifierReplacements()
+ {
+ // Arrange
+ var factory = new SpanFactory();
+ var old = new StringTextSnapshot("foo @DateTime.n.ToString() baz");
+ var changed = new StringTextSnapshot("foo @DateTime.Now.ToString() baz");
+
+ // Act and Assert
+ RunPartialParseTest(new TestEdit(14, 1, old, 3, changed, "Now"),
+ new MarkupBlock(
+ factory.Markup("foo "),
+ new ExpressionBlock(
+ factory.CodeTransition(),
+ factory.Code("DateTime.Now.ToString()").AsImplicitExpression(CSharpCodeParser.DefaultKeywords).Accepts(AcceptedCharactersInternal.NonWhiteSpace)),
+ factory.Markup(" baz")));
+ }
+
+ [Fact]
+ public void ImplicitExpressionAcceptsDotlessCommitInsertionsInStatementBlockAfterIdentifiers()
+ {
+ var factory = new SpanFactory();
+ var changed = new StringTextSnapshot("@{" + Environment.NewLine
+ + " @DateTime." + Environment.NewLine
+ + "}");
+ var old = new StringTextSnapshot("@{" + Environment.NewLine
+ + " @DateTime" + Environment.NewLine
+ + "}");
+
+ var edit = new TestEdit(15 + Environment.NewLine.Length, 0, old, 1, changed, ".");
+ using (var manager = CreateParserManager())
+ {
+ Action applyAndVerifyPartialChange = (changeToApply, expectedResult, expectedCode) =>
+ {
+ var result = manager.CheckForStructureChangesAndWait(edit);
+
+ // Assert
+ Assert.Equal(expectedResult, result);
+ Assert.Equal(1, manager.ParseCount);
+ ParserTestBase.EvaluateParseTree(manager.Parser.CurrentSyntaxTree.Root, new MarkupBlock(
+ factory.EmptyHtml(),
+ new StatementBlock(
+ factory.CodeTransition(),
+ factory.MetaCode("{").Accepts(AcceptedCharactersInternal.None),
+ factory.Code(Environment.NewLine + " ")
+ .AsStatement()
+ .AutoCompleteWith(autoCompleteString: null),
+ new ExpressionBlock(
+ factory.CodeTransition(),
+ factory.Code(expectedCode)
+ .AsImplicitExpression(CSharpCodeParser.DefaultKeywords, acceptTrailingDot: true)
+ .Accepts(AcceptedCharactersInternal.NonWhiteSpace)),
+ factory.Code(Environment.NewLine).AsStatement(),
+ factory.MetaCode("}").Accepts(AcceptedCharactersInternal.None)),
+ factory.EmptyHtml()));
+ };
+
+ manager.InitializeWithDocument(edit.OldSnapshot);
+
+ // This is the process of a dotless commit when doing "." insertions to commit intellisense changes.
+ applyAndVerifyPartialChange(edit, PartialParseResult.Accepted, "DateTime.");
+
+ old = changed;
+ changed = new StringTextSnapshot("@{" + Environment.NewLine
+ + " @DateTime.." + Environment.NewLine
+ + "}");
+ edit = new TestEdit(16 + Environment.NewLine.Length, 0, old, 1, changed, ".");
+
+ applyAndVerifyPartialChange(edit, PartialParseResult.Accepted, "DateTime..");
+
+ old = changed;
+ changed = new StringTextSnapshot("@{" + Environment.NewLine
+ + " @DateTime.Now." + Environment.NewLine
+ + "}");
+ edit = new TestEdit(16 + Environment.NewLine.Length, 0, old, 3, changed, "Now");
+
+ applyAndVerifyPartialChange(edit, PartialParseResult.Accepted, "DateTime.Now.");
+ }
+ }
+
+ [Fact]
+ public void ImplicitExpressionAcceptsDotlessCommitInsertionsInStatementBlock()
+ {
+ var factory = new SpanFactory();
+ var changed = new StringTextSnapshot("@{" + Environment.NewLine
+ + " @DateT." + Environment.NewLine
+ + "}");
+ var old = new StringTextSnapshot("@{" + Environment.NewLine
+ + " @DateT" + Environment.NewLine
+ + "}");
+
+ var edit = new TestEdit(12 + Environment.NewLine.Length, 0, old, 1, changed, ".");
+ using (var manager = CreateParserManager())
+ {
+ Action applyAndVerifyPartialChange = (changeToApply, expectedResult, expectedCode) =>
+ {
+ var result = manager.CheckForStructureChangesAndWait(edit);
+
+ // Assert
+ Assert.Equal(expectedResult, result);
+ Assert.Equal(1, manager.ParseCount);
+ ParserTestBase.EvaluateParseTree(manager.Parser.CurrentSyntaxTree.Root, new MarkupBlock(
+ factory.EmptyHtml(),
+ new StatementBlock(
+ factory.CodeTransition(),
+ factory.MetaCode("{").Accepts(AcceptedCharactersInternal.None),
+ factory.Code(Environment.NewLine + " ")
+ .AsStatement()
+ .AutoCompleteWith(autoCompleteString: null),
+ new ExpressionBlock(
+ factory.CodeTransition(),
+ factory.Code(expectedCode)
+ .AsImplicitExpression(CSharpCodeParser.DefaultKeywords, acceptTrailingDot: true)
+ .Accepts(AcceptedCharactersInternal.NonWhiteSpace)),
+ factory.Code(Environment.NewLine).AsStatement(),
+ factory.MetaCode("}").Accepts(AcceptedCharactersInternal.None)),
+ factory.EmptyHtml()));
+ };
+
+ manager.InitializeWithDocument(edit.OldSnapshot);
+
+ // This is the process of a dotless commit when doing "." insertions to commit intellisense changes.
+ applyAndVerifyPartialChange(edit, PartialParseResult.Accepted, "DateT.");
+
+ old = changed;
+ changed = new StringTextSnapshot("@{" + Environment.NewLine
+ + " @DateTime." + Environment.NewLine
+ + "}");
+ edit = new TestEdit(12 + Environment.NewLine.Length, 0, old, 3, changed, "ime");
+
+ applyAndVerifyPartialChange(edit, PartialParseResult.Accepted, "DateTime.");
+ }
+ }
+
+ [Fact]
+ public void ImplicitExpressionProvisionallyAcceptsDotlessCommitInsertions()
+ {
+ var factory = new SpanFactory();
+ var changed = new StringTextSnapshot("foo @DateT. baz");
+ var old = new StringTextSnapshot("foo @DateT baz");
+ var edit = new TestEdit(10, 0, old, 1, changed, ".");
+ using (var manager = CreateParserManager())
+ {
+ Action applyAndVerifyPartialChange = (changeToApply, expectedResult, expectedCode) =>
+ {
+ var result = manager.CheckForStructureChangesAndWait(edit);
+
+ // Assert
+ Assert.Equal(expectedResult, result);
+ Assert.Equal(1, manager.ParseCount);
+
+ ParserTestBase.EvaluateParseTree(manager.Parser.CurrentSyntaxTree.Root, new MarkupBlock(
+ factory.Markup("foo "),
+ new ExpressionBlock(
+ factory.CodeTransition(),
+ factory.Code(expectedCode).AsImplicitExpression(CSharpCodeParser.DefaultKeywords).Accepts(AcceptedCharactersInternal.NonWhiteSpace)),
+ factory.Markup(" baz")));
+ };
+
+ manager.InitializeWithDocument(edit.OldSnapshot);
+
+ // This is the process of a dotless commit when doing "." insertions to commit intellisense changes.
+ applyAndVerifyPartialChange(edit, PartialParseResult.Accepted | PartialParseResult.Provisional, "DateT.");
+
+ old = changed;
+ changed = new StringTextSnapshot("foo @DateTime. baz");
+ edit = new TestEdit(10, 0, old, 3, changed, "ime");
+
+ applyAndVerifyPartialChange(edit, PartialParseResult.Accepted | PartialParseResult.Provisional, "DateTime.");
+ }
+ }
+
+ [Fact]
+ public void ImplicitExpressionProvisionallyAcceptsDotlessCommitInsertionsAfterIdentifiers()
+ {
+ var factory = new SpanFactory();
+ var changed = new StringTextSnapshot("foo @DateTime. baz");
+ var old = new StringTextSnapshot("foo @DateTime baz");
+ var edit = new TestEdit(13, 0, old, 1, changed, ".");
+ using (var manager = CreateParserManager())
+ {
+ Action applyAndVerifyPartialChange = (changeToApply, expectedResult, expectedCode) =>
+ {
+ var result = manager.CheckForStructureChangesAndWait(edit);
+
+ // Assert
+ Assert.Equal(expectedResult, result);
+ Assert.Equal(1, manager.ParseCount);
+
+ ParserTestBase.EvaluateParseTree(manager.Parser.CurrentSyntaxTree.Root, new MarkupBlock(
+ factory.Markup("foo "),
+ new ExpressionBlock(
+ factory.CodeTransition(),
+ factory.Code(expectedCode).AsImplicitExpression(CSharpCodeParser.DefaultKeywords).Accepts(AcceptedCharactersInternal.NonWhiteSpace)),
+ factory.Markup(" baz")));
+ };
+
+ manager.InitializeWithDocument(edit.OldSnapshot);
+
+ // This is the process of a dotless commit when doing "." insertions to commit intellisense changes.
+ applyAndVerifyPartialChange(edit, PartialParseResult.Accepted | PartialParseResult.Provisional, "DateTime.");
+
+ old = changed;
+ changed = new StringTextSnapshot("foo @DateTime.. baz");
+ edit = new TestEdit(14, 0, old, 1, changed, ".");
+
+ applyAndVerifyPartialChange(edit, PartialParseResult.Accepted | PartialParseResult.Provisional, "DateTime..");
+
+ old = changed;
+ changed = new StringTextSnapshot("foo @DateTime.Now. baz");
+ edit = new TestEdit(14, 0, old, 3, changed, "Now");
+
+ applyAndVerifyPartialChange(edit, PartialParseResult.Accepted | PartialParseResult.Provisional, "DateTime.Now.");
+ }
+ }
+
+ [Fact]
+ public void ImplicitExpressionProvisionallyAcceptsCaseInsensitiveDotlessCommitInsertions_NewRoslynIntegration()
+ {
+ var factory = new SpanFactory();
+ var old = new StringTextSnapshot("foo @date baz");
+ var changed = new StringTextSnapshot("foo @date. baz");
+ var edit = new TestEdit(9, 0, old, 1, changed, ".");
+ using (var manager = CreateParserManager())
+ {
+ Action applyAndVerifyPartialChange = (changeToApply, expectedResult, expectedCode) =>
+ {
+ var result = manager.CheckForStructureChangesAndWait(edit);
+
+ // Assert
+ Assert.Equal(expectedResult, result);
+ Assert.Equal(1, manager.ParseCount);
+
+ ParserTestBase.EvaluateParseTree(manager.Parser.CurrentSyntaxTree.Root, new MarkupBlock(
+ factory.Markup("foo "),
+ new ExpressionBlock(
+ factory.CodeTransition(),
+ factory.Code(expectedCode).AsImplicitExpression(CSharpCodeParser.DefaultKeywords).Accepts(AcceptedCharactersInternal.NonWhiteSpace)),
+ factory.Markup(" baz")));
+ };
+
+ manager.InitializeWithDocument(edit.OldSnapshot);
+
+ // This is the process of a dotless commit when doing "." insertions to commit intellisense changes.
+
+ // @date => @date.
+ applyAndVerifyPartialChange(edit, PartialParseResult.Accepted | PartialParseResult.Provisional, "date.");
+
+ old = changed;
+ changed = new StringTextSnapshot("foo @date baz");
+ edit = new TestEdit(9, 1, old, 0, changed, "");
+
+ // @date. => @date
+ applyAndVerifyPartialChange(edit, PartialParseResult.Accepted, "date");
+
+ old = changed;
+ changed = new StringTextSnapshot("foo @DateTime baz");
+ edit = new TestEdit(5, 4, old, 8, changed, "DateTime");
+
+ // @date => @DateTime
+ applyAndVerifyPartialChange(edit, PartialParseResult.Accepted, "DateTime");
+
+ old = changed;
+ changed = new StringTextSnapshot("foo @DateTime. baz");
+ edit = new TestEdit(13, 0, old, 1, changed, ".");
+
+ // @DateTime => @DateTime.
+ applyAndVerifyPartialChange(edit, PartialParseResult.Accepted | PartialParseResult.Provisional, "DateTime.");
+ }
+ }
+
+ [Fact]
+ public void ImplicitExpressionProvisionallyAcceptsDeleteOfIdentifierPartsIfDotRemains()
+ {
+ var factory = new SpanFactory();
+ var changed = new StringTextSnapshot("foo @User. baz");
+ var old = new StringTextSnapshot("foo @User.Name baz");
+ RunPartialParseTest(new TestEdit(10, 4, old, 0, changed, string.Empty),
+ new MarkupBlock(
+ factory.Markup("foo "),
+ new ExpressionBlock(
+ factory.CodeTransition(),
+ factory.Code("User.").AsImplicitExpression(CSharpCodeParser.DefaultKeywords).Accepts(AcceptedCharactersInternal.NonWhiteSpace)),
+ factory.Markup(" baz")),
+ additionalFlags: PartialParseResult.Provisional);
+ }
+
+ [Fact]
+ public void ImplicitExpressionAcceptsDeleteOfIdentifierPartsIfSomeOfIdentifierRemains()
+ {
+ var factory = new SpanFactory();
+ var changed = new StringTextSnapshot("foo @Us baz");
+ var old = new StringTextSnapshot("foo @User baz");
+ RunPartialParseTest(new TestEdit(7, 2, old, 0, changed, string.Empty),
+ new MarkupBlock(
+ factory.Markup("foo "),
+ new ExpressionBlock(
+ factory.CodeTransition(),
+ factory.Code("Us").AsImplicitExpression(CSharpCodeParser.DefaultKeywords).Accepts(AcceptedCharactersInternal.NonWhiteSpace)),
+ factory.Markup(" baz")));
+ }
+
+ [Fact]
+ public void ImplicitExpressionProvisionallyAcceptsMultipleInsertionIfItCausesIdentifierExpansionAndTrailingDot()
+ {
+ var factory = new SpanFactory();
+ var changed = new StringTextSnapshot("foo @User. baz");
+ var old = new StringTextSnapshot("foo @U baz");
+ RunPartialParseTest(new TestEdit(6, 0, old, 4, changed, "ser."),
+ new MarkupBlock(
+ factory.Markup("foo "),
+ new ExpressionBlock(
+ factory.CodeTransition(),
+ factory.Code("User.").AsImplicitExpression(CSharpCodeParser.DefaultKeywords).Accepts(AcceptedCharactersInternal.NonWhiteSpace)),
+ factory.Markup(" baz")),
+ additionalFlags: PartialParseResult.Provisional);
+ }
+
+ [Fact]
+ public void ImplicitExpressionAcceptsMultipleInsertionIfItOnlyCausesIdentifierExpansion()
+ {
+ var factory = new SpanFactory();
+ var changed = new StringTextSnapshot("foo @barbiz baz");
+ var old = new StringTextSnapshot("foo @bar baz");
+ RunPartialParseTest(new TestEdit(8, 0, old, 3, changed, "biz"),
+ new MarkupBlock(
+ factory.Markup("foo "),
+ new ExpressionBlock(
+ factory.CodeTransition(),
+ factory.Code("barbiz").AsImplicitExpression(CSharpCodeParser.DefaultKeywords).Accepts(AcceptedCharactersInternal.NonWhiteSpace)),
+ factory.Markup(" baz")));
+ }
+
+ [Fact]
+ public void ImplicitExpressionAcceptsIdentifierExpansionAtEndOfNonWhitespaceCharacters()
+ {
+ var factory = new SpanFactory();
+ var changed = new StringTextSnapshot("@{" + Environment.NewLine
+ + " @food" + Environment.NewLine
+ + "}");
+ var old = new StringTextSnapshot("@{" + Environment.NewLine
+ + " @foo" + Environment.NewLine
+ + "}");
+ RunPartialParseTest(new TestEdit(10 + Environment.NewLine.Length, 0, old, 1, changed, "d"),
+ new MarkupBlock(
+ factory.EmptyHtml(),
+ new StatementBlock(
+ factory.CodeTransition(),
+ factory.MetaCode("{").Accepts(AcceptedCharactersInternal.None),
+ factory.Code(Environment.NewLine + " ")
+ .AsStatement()
+ .AutoCompleteWith(autoCompleteString: null),
+ new ExpressionBlock(
+ factory.CodeTransition(),
+ factory.Code("food")
+ .AsImplicitExpression(CSharpCodeParser.DefaultKeywords, acceptTrailingDot: true)
+ .Accepts(AcceptedCharactersInternal.NonWhiteSpace)),
+ factory.Code(Environment.NewLine).AsStatement(),
+ factory.MetaCode("}").Accepts(AcceptedCharactersInternal.None)),
+ factory.EmptyHtml()));
+ }
+
+ [Fact]
+ public void ImplicitExpressionAcceptsIdentifierAfterDotAtEndOfNonWhitespaceCharacters()
+ {
+ var factory = new SpanFactory();
+ var changed = new StringTextSnapshot("@{" + Environment.NewLine
+ + " @foo.d" + Environment.NewLine
+ + "}");
+ var old = new StringTextSnapshot("@{" + Environment.NewLine
+ + " @foo." + Environment.NewLine
+ + "}");
+ RunPartialParseTest(new TestEdit(11 + Environment.NewLine.Length, 0, old, 1, changed, "d"),
+ new MarkupBlock(
+ factory.EmptyHtml(),
+ new StatementBlock(
+ factory.CodeTransition(),
+ factory.MetaCode("{").Accepts(AcceptedCharactersInternal.None),
+ factory.Code(Environment.NewLine + " ")
+ .AsStatement()
+ .AutoCompleteWith(autoCompleteString: null),
+ new ExpressionBlock(
+ factory.CodeTransition(),
+ factory.Code("foo.d")
+ .AsImplicitExpression(CSharpCodeParser.DefaultKeywords, acceptTrailingDot: true)
+ .Accepts(AcceptedCharactersInternal.NonWhiteSpace)),
+ factory.Code(Environment.NewLine).AsStatement(),
+ factory.MetaCode("}").Accepts(AcceptedCharactersInternal.None)),
+ factory.EmptyHtml()));
+ }
+
+ [Fact]
+ public void ImplicitExpressionAcceptsDotAtEndOfNonWhitespaceCharacters()
+ {
+ var factory = new SpanFactory();
+ var changed = new StringTextSnapshot("@{" + Environment.NewLine
+ + " @foo." + Environment.NewLine
+ + "}");
+ var old = new StringTextSnapshot("@{" + Environment.NewLine
+ + " @foo" + Environment.NewLine
+ + "}");
+ RunPartialParseTest(new TestEdit(10 + Environment.NewLine.Length, 0, old, 1, changed, "."),
+ new MarkupBlock(
+ factory.EmptyHtml(),
+ new StatementBlock(
+ factory.CodeTransition(),
+ factory.MetaCode("{").Accepts(AcceptedCharactersInternal.None),
+ factory.Code(Environment.NewLine + " ")
+ .AsStatement()
+ .AutoCompleteWith(autoCompleteString: null),
+ new ExpressionBlock(
+ factory.CodeTransition(),
+ factory.Code(@"foo.")
+ .AsImplicitExpression(CSharpCodeParser.DefaultKeywords, acceptTrailingDot: true)
+ .Accepts(AcceptedCharactersInternal.NonWhiteSpace)),
+ factory.Code(Environment.NewLine).AsStatement(),
+ factory.MetaCode("}").Accepts(AcceptedCharactersInternal.None)),
+ factory.EmptyHtml()));
+ }
+
+ [Fact]
+ public void ImplicitExpressionRejectsChangeWhichWouldHaveBeenAcceptedIfLastChangeWasProvisionallyAcceptedOnDifferentSpan()
+ {
+ var factory = new SpanFactory();
+
+ // Arrange
+ var dotTyped = new TestEdit(8, 0, new StringTextSnapshot("foo @foo @bar"), 1, new StringTextSnapshot("foo @foo. @bar"), ".");
+ var charTyped = new TestEdit(14, 0, new StringTextSnapshot("foo @foo. @bar"), 1, new StringTextSnapshot("foo @foo. @barb"), "b");
+ using (var manager = CreateParserManager())
+ {
+ manager.InitializeWithDocument(dotTyped.OldSnapshot);
+
+ // Apply the dot change
+ Assert.Equal(PartialParseResult.Provisional | PartialParseResult.Accepted, manager.CheckForStructureChangesAndWait(dotTyped));
+
+ // Act (apply the identifier start char change)
+ var result = manager.CheckForStructureChangesAndWait(charTyped);
+
+ // Assert
+ Assert.Equal(PartialParseResult.Rejected, result);
+ Assert.False(manager.Parser.LastResultProvisional, "LastResultProvisional flag should have been cleared but it was not");
+ ParserTestBase.EvaluateParseTree(manager.Parser.CurrentSyntaxTree.Root,
+ new MarkupBlock(
+ factory.Markup("foo "),
+ new ExpressionBlock(
+ factory.CodeTransition(),
+ factory.Code("foo")
+ .AsImplicitExpression(CSharpCodeParser.DefaultKeywords)
+ .Accepts(AcceptedCharactersInternal.NonWhiteSpace)),
+ factory.Markup(". "),
+ new ExpressionBlock(
+ factory.CodeTransition(),
+ factory.Code("barb")
+ .AsImplicitExpression(CSharpCodeParser.DefaultKeywords)
+ .Accepts(AcceptedCharactersInternal.NonWhiteSpace)),
+ factory.EmptyHtml()));
+ }
+ }
+
+ [Fact]
+ public void ImplicitExpressionAcceptsIdentifierTypedAfterDotIfLastChangeWasProvisionalAcceptanceOfDot()
+ {
+ var factory = new SpanFactory();
+
+ // Arrange
+ var dotTyped = new TestEdit(8, 0, new StringTextSnapshot("foo @foo bar"), 1, new StringTextSnapshot("foo @foo. bar"), ".");
+ var charTyped = new TestEdit(9, 0, new StringTextSnapshot("foo @foo. bar"), 1, new StringTextSnapshot("foo @foo.b bar"), "b");
+ using (var manager = CreateParserManager())
+ {
+ manager.InitializeWithDocument(dotTyped.OldSnapshot);
+
+ // Apply the dot change
+ Assert.Equal(PartialParseResult.Provisional | PartialParseResult.Accepted, manager.CheckForStructureChangesAndWait(dotTyped));
+
+ // Act (apply the identifier start char change)
+ var result = manager.CheckForStructureChangesAndWait(charTyped);
+
+ // Assert
+ Assert.Equal(PartialParseResult.Accepted, result);
+ Assert.False(manager.Parser.LastResultProvisional, "LastResultProvisional flag should have been cleared but it was not");
+ ParserTestBase.EvaluateParseTree(manager.Parser.CurrentSyntaxTree.Root,
+ new MarkupBlock(
+ factory.Markup("foo "),
+ new ExpressionBlock(
+ factory.CodeTransition(),
+ factory.Code("foo.b")
+ .AsImplicitExpression(CSharpCodeParser.DefaultKeywords)
+ .Accepts(AcceptedCharactersInternal.NonWhiteSpace)),
+ factory.Markup(" bar")));
+ }
+ }
+
+ [Fact]
+ public void ImplicitExpressionProvisionallyAcceptsDotAfterIdentifierInMarkup()
+ {
+ var factory = new SpanFactory();
+ var changed = new StringTextSnapshot("foo @foo. bar");
+ var old = new StringTextSnapshot("foo @foo bar");
+ RunPartialParseTest(new TestEdit(8, 0, old, 1, changed, "."),
+ new MarkupBlock(
+ factory.Markup("foo "),
+ new ExpressionBlock(
+ factory.CodeTransition(),
+ factory.Code("foo.")
+ .AsImplicitExpression(CSharpCodeParser.DefaultKeywords)
+ .Accepts(AcceptedCharactersInternal.NonWhiteSpace)),
+ factory.Markup(" bar")),
+ additionalFlags: PartialParseResult.Provisional);
+ }
+
+ [Fact]
+ public void ImplicitExpressionAcceptsAdditionalIdentifierCharactersIfEndOfSpanIsIdentifier()
+ {
+ var factory = new SpanFactory();
+ var changed = new StringTextSnapshot("foo @foob bar");
+ var old = new StringTextSnapshot("foo @foo bar");
+ RunPartialParseTest(new TestEdit(8, 0, old, 1, changed, "b"),
+ new MarkupBlock(
+ factory.Markup("foo "),
+ new ExpressionBlock(
+ factory.CodeTransition(),
+ factory.Code("foob")
+ .AsImplicitExpression(CSharpCodeParser.DefaultKeywords)
+ .Accepts(AcceptedCharactersInternal.NonWhiteSpace)),
+ factory.Markup(" bar")));
+ }
+
+ [Fact]
+ public void ImplicitExpressionAcceptsAdditionalIdentifierStartCharactersIfEndOfSpanIsDot()
+ {
+ var factory = new SpanFactory();
+ var changed = new StringTextSnapshot("@{@foo.b}");
+ var old = new StringTextSnapshot("@{@foo.}");
+ RunPartialParseTest(new TestEdit(7, 0, old, 1, changed, "b"),
+ new MarkupBlock(
+ factory.EmptyHtml(),
+ new StatementBlock(
+ factory.CodeTransition(),
+ factory.MetaCode("{").Accepts(AcceptedCharactersInternal.None),
+ factory.EmptyCSharp()
+ .AsStatement()
+ .AutoCompleteWith(autoCompleteString: null),
+ new ExpressionBlock(
+ factory.CodeTransition(),
+ factory.Code("foo.b")
+ .AsImplicitExpression(CSharpCodeParser.DefaultKeywords, acceptTrailingDot: true)
+ .Accepts(AcceptedCharactersInternal.NonWhiteSpace)),
+ factory.EmptyCSharp().AsStatement(),
+ factory.MetaCode("}").Accepts(AcceptedCharactersInternal.None)),
+ factory.EmptyHtml()));
+ }
+
+ [Fact]
+ public void ImplicitExpressionAcceptsDotIfTrailingDotsAreAllowed()
+ {
+ var factory = new SpanFactory();
+ var changed = new StringTextSnapshot("@{@foo.}");
+ var old = new StringTextSnapshot("@{@foo}");
+ RunPartialParseTest(new TestEdit(6, 0, old, 1, changed, "."),
+ new MarkupBlock(
+ factory.EmptyHtml(),
+ new StatementBlock(
+ factory.CodeTransition(),
+ factory.MetaCode("{").Accepts(AcceptedCharactersInternal.None),
+ factory.EmptyCSharp()
+ .AsStatement()
+ .AutoCompleteWith(autoCompleteString: null),
+ new ExpressionBlock(
+ factory.CodeTransition(),
+ factory.Code("foo.")
+ .AsImplicitExpression(CSharpCodeParser.DefaultKeywords, acceptTrailingDot: true)
+ .Accepts(AcceptedCharactersInternal.NonWhiteSpace)),
+ factory.EmptyCSharp().AsStatement(),
+ factory.MetaCode("}").Accepts(AcceptedCharactersInternal.None)),
+ factory.EmptyHtml()));
+ }
+
+ [Fact]
+ public void ImplicitExpressionCorrectlyTriggersReparseIfIfKeywordTyped()
+ {
+ RunTypeKeywordTest("if");
+ }
+
+ [Fact]
+ public void ImplicitExpressionCorrectlyTriggersReparseIfDoKeywordTyped()
+ {
+ RunTypeKeywordTest("do");
+ }
+
+ [Fact]
+ public void ImplicitExpressionCorrectlyTriggersReparseIfTryKeywordTyped()
+ {
+ RunTypeKeywordTest("try");
+ }
+
+ [Fact]
+ public void ImplicitExpressionCorrectlyTriggersReparseIfForKeywordTyped()
+ {
+ RunTypeKeywordTest("for");
+ }
+
+ [Fact]
+ public void ImplicitExpressionCorrectlyTriggersReparseIfForEachKeywordTyped()
+ {
+ RunTypeKeywordTest("foreach");
+ }
+
+ [Fact]
+ public void ImplicitExpressionCorrectlyTriggersReparseIfWhileKeywordTyped()
+ {
+ RunTypeKeywordTest("while");
+ }
+
+ [Fact]
+ public void ImplicitExpressionCorrectlyTriggersReparseIfSwitchKeywordTyped()
+ {
+ RunTypeKeywordTest("switch");
+ }
+
+ [Fact]
+ public void ImplicitExpressionCorrectlyTriggersReparseIfLockKeywordTyped()
+ {
+ RunTypeKeywordTest("lock");
+ }
+
+ [Fact]
+ public void ImplicitExpressionCorrectlyTriggersReparseIfUsingKeywordTyped()
+ {
+ RunTypeKeywordTest("using");
+ }
+
+ [Fact]
+ public void ImplicitExpressionCorrectlyTriggersReparseIfSectionKeywordTyped()
+ {
+ RunTypeKeywordTest("section");
+ }
+
+ [Fact]
+ public void ImplicitExpressionCorrectlyTriggersReparseIfInheritsKeywordTyped()
+ {
+ RunTypeKeywordTest("inherits");
+ }
+
+ [Fact]
+ public void ImplicitExpressionCorrectlyTriggersReparseIfFunctionsKeywordTyped()
+ {
+ RunTypeKeywordTest("functions");
+ }
+
+ [Fact]
+ public void ImplicitExpressionCorrectlyTriggersReparseIfNamespaceKeywordTyped()
+ {
+ RunTypeKeywordTest("namespace");
+ }
+
+ [Fact]
+ public void ImplicitExpressionCorrectlyTriggersReparseIfClassKeywordTyped()
+ {
+ RunTypeKeywordTest("class");
+ }
+
+ private static TestEdit CreateInsertionChange(string initialText, int insertionLocation, string insertionText)
+ {
+ var changedText = initialText.Insert(insertionLocation, insertionText);
+ var sourceChange = new SourceChange(insertionLocation, 0, insertionText);
+ var oldSnapshot = new StringTextSnapshot(initialText);
+ var changedSnapshot = new StringTextSnapshot(changedText);
+ return new TestEdit
+ {
+ Change = sourceChange,
+ OldSnapshot = oldSnapshot,
+ NewSnapshot = changedSnapshot,
+ };
+ }
+
+ private static void RunFullReparseTest(TestEdit edit, PartialParseResult additionalFlags = (PartialParseResult)0)
+ {
+ // Arrange
+ using (var manager = CreateParserManager())
+ {
+ manager.InitializeWithDocument(edit.OldSnapshot);
+
+ // Act
+ var result = manager.CheckForStructureChangesAndWait(edit);
+
+ // Assert
+ Assert.Equal(PartialParseResult.Rejected | additionalFlags, result);
+ Assert.Equal(2, manager.ParseCount);
+ }
+ }
+
+ private static void RunPartialParseTest(TestEdit edit, Block newTreeRoot, PartialParseResult additionalFlags = (PartialParseResult)0)
+ {
+ // Arrange
+ using (var manager = CreateParserManager())
+ {
+ manager.InitializeWithDocument(edit.OldSnapshot);
+
+ // Act
+ var result = manager.CheckForStructureChangesAndWait(edit);
+
+ // Assert
+ Assert.Equal(PartialParseResult.Accepted | additionalFlags, result);
+ Assert.Equal(1, manager.ParseCount);
+ ParserTestBase.EvaluateParseTree(manager.Parser.CurrentSyntaxTree.Root, newTreeRoot);
+ }
+ }
+
+ private static TestParserManager CreateParserManager()
+ {
+ var parser = new RazorEditorParser(CreateTemplateEngine(), TestLinePragmaFileName);
+ return new TestParserManager(parser);
+ }
+
+ private static RazorTemplateEngine CreateTemplateEngine(
+ string path = TestLinePragmaFileName,
+ IEnumerable tagHelpers = null)
+ {
+ var engine = RazorEngine.CreateDesignTime(builder =>
+ {
+ RazorExtensions.Register(builder);
+
+ if (tagHelpers != null)
+ {
+ builder.AddTagHelpers(tagHelpers);
+ }
+ });
+
+ // GetImports on RazorTemplateEngine will at least check that the item exists, so we need to pretend
+ // that it does.
+ var items = new List();
+ items.Add(new TestRazorProjectItem(path));
+
+ var project = new TestRazorProject(items);
+
+ var templateEngine = new RazorTemplateEngine(engine, project);
+ templateEngine.Options.DefaultImports = RazorSourceDocument.Create("@addTagHelper *, Test", "_TestImports.cshtml");
+ return templateEngine;
+ }
+
+ private static void RunTypeKeywordTest(string keyword)
+ {
+ var before = "@" + keyword.Substring(0, keyword.Length - 1);
+ var after = "@" + keyword;
+ var changed = new StringTextSnapshot(after);
+ var old = new StringTextSnapshot(before);
+ var change = new SourceChange(keyword.Length, 0, keyword[keyword.Length - 1].ToString());
+ var edit = new TestEdit
+ {
+ Change = change,
+ NewSnapshot = changed,
+ OldSnapshot = old
+ };
+ RunFullReparseTest(edit, additionalFlags: PartialParseResult.SpanContextChanged);
+ }
+
+ private static void DoWithTimeoutIfNotDebugging(Func withTimeout)
+ {
+#if DEBUG
+ if (Debugger.IsAttached)
+ {
+ withTimeout(Timeout.Infinite);
+ }
+ else
+ {
+#endif
+ Assert.True(withTimeout((int)TimeSpan.FromSeconds(1).TotalMilliseconds), "Timeout expired!");
+#if DEBUG
+ }
+#endif
+ }
+
+ private class TestParserManager : IDisposable
+ {
+ public int ParseCount;
+
+ private readonly ManualResetEventSlim _parserComplete;
+
+ public TestParserManager(RazorEditorParser parser)
+ {
+ _parserComplete = new ManualResetEventSlim();
+ ParseCount = 0;
+ Parser = parser;
+ parser.DocumentParseComplete += (sender, args) =>
+ {
+ Interlocked.Increment(ref ParseCount);
+ _parserComplete.Set();
+ };
+ }
+
+ public RazorEditorParser Parser { get; }
+
+ public void InitializeWithDocument(ITextSnapshot snapshot)
+ {
+ var initialChange = new SourceChange(0, 0, string.Empty);
+ var edit = new TestEdit
+ {
+ Change = initialChange,
+ OldSnapshot = snapshot,
+ NewSnapshot = snapshot
+ };
+ CheckForStructureChangesAndWait(edit);
+ }
+
+ public PartialParseResult CheckForStructureChangesAndWait(TestEdit edit)
+ {
+ var result = Parser.CheckForStructureChanges(edit.Change, edit.NewSnapshot);
+ if (result.HasFlag(PartialParseResult.Rejected))
+ {
+ WaitForParse();
+ }
+ return result;
+ }
+
+ public void WaitForParse()
+ {
+ DoWithTimeoutIfNotDebugging(_parserComplete.Wait); // Wait for the parse to finish
+ _parserComplete.Reset();
+ }
+
+ public void Dispose()
+ {
+ Parser.Dispose();
+ }
+ }
+
+ private class TestEdit
+ {
+ public TestEdit()
+ {
+ }
+
+ public TestEdit(int position, int oldLength, ITextSnapshot oldSnapshot, int newLength, ITextSnapshot newSnapshot, string newText)
+ {
+ Change = new SourceChange(position, oldLength, newText);
+ OldSnapshot = oldSnapshot;
+ NewSnapshot = newSnapshot;
+ }
+
+ public SourceChange Change { get; set; }
+
+ public ITextSnapshot OldSnapshot { get; set; }
+
+ public ITextSnapshot NewSnapshot { get; set; }
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/StringTextSnapshot.cs b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/StringTextSnapshot.cs
new file mode 100644
index 0000000000..c6e183029b
--- /dev/null
+++ b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/StringTextSnapshot.cs
@@ -0,0 +1,106 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// 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.IO;
+using Microsoft.VisualStudio.Text;
+using Microsoft.VisualStudio.Utilities;
+
+namespace Microsoft.AspNetCore.Razor.Language.Legacy
+{
+ public class StringTextSnapshot : ITextSnapshot
+ {
+ private readonly string _content;
+
+ public StringTextSnapshot(string content)
+ {
+ _content = content;
+ }
+
+ public char this[int position] => _content[position];
+
+ public VisualStudio.Text.ITextBuffer TextBuffer => throw new NotImplementedException();
+
+ public IContentType ContentType => throw new NotImplementedException();
+
+ public ITextVersion Version => throw new NotImplementedException();
+
+ public int Length => _content.Length;
+
+ public int LineCount => throw new NotImplementedException();
+
+ public IEnumerable Lines => throw new NotImplementedException();
+
+ public void CopyTo(int sourceIndex, char[] destination, int destinationIndex, int count)
+ {
+ _content.CopyTo(sourceIndex, destination, destinationIndex, count);
+ }
+
+ public ITrackingPoint CreateTrackingPoint(int position, PointTrackingMode trackingMode)
+ {
+ throw new NotImplementedException();
+ }
+
+ public ITrackingPoint CreateTrackingPoint(int position, PointTrackingMode trackingMode, TrackingFidelityMode trackingFidelity)
+ {
+ throw new NotImplementedException();
+ }
+
+ public ITrackingSpan CreateTrackingSpan(VisualStudio.Text.Span span, SpanTrackingMode trackingMode)
+ {
+ throw new NotImplementedException();
+ }
+
+ public ITrackingSpan CreateTrackingSpan(VisualStudio.Text.Span span, SpanTrackingMode trackingMode, TrackingFidelityMode trackingFidelity)
+ {
+ throw new NotImplementedException();
+ }
+
+ public ITrackingSpan CreateTrackingSpan(int start, int length, SpanTrackingMode trackingMode)
+ {
+ throw new NotImplementedException();
+ }
+
+ public ITrackingSpan CreateTrackingSpan(int start, int length, SpanTrackingMode trackingMode, TrackingFidelityMode trackingFidelity)
+ {
+ throw new NotImplementedException();
+ }
+
+ public ITextSnapshotLine GetLineFromLineNumber(int lineNumber)
+ {
+ throw new NotImplementedException();
+ }
+
+ public ITextSnapshotLine GetLineFromPosition(int position)
+ {
+ throw new NotImplementedException();
+ }
+
+ public int GetLineNumberFromPosition(int position)
+ {
+ throw new NotImplementedException();
+ }
+
+ public string GetText(VisualStudio.Text.Span span)
+ {
+ throw new NotImplementedException();
+ }
+
+ public string GetText(int startIndex, int length) => _content.Substring(startIndex, length);
+
+ public string GetText() => _content;
+
+ public char[] ToCharArray(int startIndex, int length) => _content.ToCharArray();
+
+ public void Write(TextWriter writer, VisualStudio.Text.Span span)
+ {
+ throw new NotImplementedException();
+ }
+
+ public void Write(TextWriter writer)
+ {
+ throw new NotImplementedException();
+ }
+ }
+}
diff --git a/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Language/TestBoundAttributeDescriptorBuilderExtensions.cs b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Test/TestBoundAttributeDescriptorBuilderExtensions.cs
similarity index 97%
rename from test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Language/TestBoundAttributeDescriptorBuilderExtensions.cs
rename to test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Test/TestBoundAttributeDescriptorBuilderExtensions.cs
index 23987d03bf..d784d7b1c0 100644
--- a/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Language/TestBoundAttributeDescriptorBuilderExtensions.cs
+++ b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Test/TestBoundAttributeDescriptorBuilderExtensions.cs
@@ -2,8 +2,9 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
+using Microsoft.AspNetCore.Razor.Language;
-namespace Microsoft.AspNetCore.Razor.Language
+namespace Microsoft.VisualStudio.LanguageServices.Razor
{
public static class TestBoundAttributeDescriptorBuilderExtensions
{
diff --git a/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Language/TestRequiredAttributeDescriptorBuilderExtensions.cs b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Test/TestRequiredAttributeDescriptorBuilderExtensions.cs
similarity index 95%
rename from test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Language/TestRequiredAttributeDescriptorBuilderExtensions.cs
rename to test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Test/TestRequiredAttributeDescriptorBuilderExtensions.cs
index da638e1750..16c4ea9100 100644
--- a/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Language/TestRequiredAttributeDescriptorBuilderExtensions.cs
+++ b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Test/TestRequiredAttributeDescriptorBuilderExtensions.cs
@@ -2,8 +2,9 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
+using Microsoft.AspNetCore.Razor.Language;
-namespace Microsoft.AspNetCore.Razor.Language
+namespace Microsoft.VisualStudio.LanguageServices.Razor
{
public static class TestRequiredAttributeDescriptorBuilderExtensions
{
diff --git a/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Language/TestTagHelperDescriptorBuilderExtensions.cs b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Test/TestTagHelperDescriptorBuilderExtensions.cs
similarity index 97%
rename from test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Language/TestTagHelperDescriptorBuilderExtensions.cs
rename to test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Test/TestTagHelperDescriptorBuilderExtensions.cs
index 5bc81a7792..c32a61580e 100644
--- a/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Language/TestTagHelperDescriptorBuilderExtensions.cs
+++ b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Test/TestTagHelperDescriptorBuilderExtensions.cs
@@ -2,8 +2,9 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
+using Microsoft.AspNetCore.Razor.Language;
-namespace Microsoft.AspNetCore.Razor.Language
+namespace Microsoft.VisualStudio.LanguageServices.Razor
{
public static class TestTagHelperDescriptorBuilderExtensions
{
diff --git a/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Language/TestTagMatchingRuleDescriptorBuilderExtensions.cs b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Test/TestTagMatchingRuleDescriptorBuilderExtensions.cs
similarity index 95%
rename from test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Language/TestTagMatchingRuleDescriptorBuilderExtensions.cs
rename to test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Test/TestTagMatchingRuleDescriptorBuilderExtensions.cs
index f052818fe6..5373498774 100644
--- a/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Language/TestTagMatchingRuleDescriptorBuilderExtensions.cs
+++ b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Test/TestTagMatchingRuleDescriptorBuilderExtensions.cs
@@ -2,8 +2,9 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
+using Microsoft.AspNetCore.Razor.Language;
-namespace Microsoft.AspNetCore.Razor.Language
+namespace Microsoft.VisualStudio.LanguageServices.Razor
{
public static class TestTagMatchingRuleDescriptorBuilderExtensions
{