diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/DefaultTextViewRazorDocumentTrackerService.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/DefaultTextViewRazorDocumentTrackerService.cs
index 250081cb20..1d175017d1 100644
--- a/src/Microsoft.VisualStudio.LanguageServices.Razor/DefaultTextViewRazorDocumentTrackerService.cs
+++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/DefaultTextViewRazorDocumentTrackerService.cs
@@ -2,6 +2,7 @@
// 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.Collections.ObjectModel;
using System.ComponentModel.Composition;
using Microsoft.CodeAnalysis;
@@ -165,5 +166,12 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor
return project.IsCapabilityMatch("DotNetCoreWeb");
}
+
+ public static IEnumerable GetTextViews(ITextBuffer textBuffer)
+ {
+ // TODO: Extract text views from buffer
+
+ return new[] { (ITextView)null };
+ }
}
}
diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/BackgroundParser.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/Editor/BackgroundParser.cs
similarity index 84%
rename from src/Microsoft.VisualStudio.LanguageServices.Razor/BackgroundParser.cs
rename to src/Microsoft.VisualStudio.LanguageServices.Razor/Editor/BackgroundParser.cs
index b71f4d8814..b3b290af0a 100644
--- a/src/Microsoft.VisualStudio.LanguageServices.Razor/BackgroundParser.cs
+++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/Editor/BackgroundParser.cs
@@ -11,7 +11,7 @@ using Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.Language.Legacy;
using Microsoft.VisualStudio.Text;
-namespace Microsoft.VisualStudio.LanguageServices.Razor
+namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor
{
internal class BackgroundParser : IDisposable
{
@@ -29,7 +29,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor
///
/// Fired on the main thread.
///
- public event EventHandler ResultsReady;
+ public event EventHandler ResultsReady;
public bool IsIdle
{
@@ -62,46 +62,14 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor
return _main.Lock();
}
- protected virtual void OnResultsReady(DocumentParseCompleteEventArgs args)
+ protected virtual void OnResultsReady(DocumentStructureChangedEventArgs args)
{
- var handler = ResultsReady;
- if (handler != null)
+ using (SynchronizeMainThreadState())
{
- handler(this, args);
+ ResultsReady?.Invoke(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
@@ -155,7 +123,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor
SetThreadId(Thread.CurrentThread.ManagedThreadId);
}
- public event EventHandler ResultsReady;
+ public event EventHandler ResultsReady;
public CancellationToken CancelToken
{
@@ -187,7 +155,8 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor
public void QueueChange(Edit edit)
{
- EnsureOnThread();
+ // Any thread can queue a change.
+
lock (_stateLock)
{
// CurrentParcel token source is not null ==> There's a parse underway
@@ -217,7 +186,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor
}
}
- public void ReturnParcel(DocumentParseCompleteEventArgs args)
+ public void ReturnParcel(DocumentStructureChangedEventArgs args)
{
lock (_stateLock)
{
@@ -307,7 +276,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor
{
try
{
- DocumentParseCompleteEventArgs args = null;
+ DocumentStructureChangedEventArgs args = null;
using (var linkedCancel = CancellationTokenSource.CreateLinkedTokenSource(_shutdownToken, parcel.CancelToken))
{
if (!linkedCancel.IsCancellationRequested)
@@ -333,14 +302,12 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor
// 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(
+ args = new DocumentStructureChangedEventArgs(
finalEdit.Change,
finalEdit.Snapshot,
- treeStructureChanged,
results);
}
else
@@ -416,4 +383,4 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor
public ITextSnapshot Snapshot { get; set; }
}
}
-}
\ No newline at end of file
+}
diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/Editor/DocumentStructureChangedEventArgs.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/Editor/DocumentStructureChangedEventArgs.cs
new file mode 100644
index 0000000000..c5ff0a249f
--- /dev/null
+++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/Editor/DocumentStructureChangedEventArgs.cs
@@ -0,0 +1,37 @@
+// 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.Editor
+{
+ internal sealed class DocumentStructureChangedEventArgs : EventArgs
+ {
+ public DocumentStructureChangedEventArgs(
+ SourceChange change,
+ ITextSnapshot snapshot,
+ RazorCodeDocument codeDocument)
+ {
+ SourceChange = change;
+ Snapshot = snapshot;
+ CodeDocument = codeDocument;
+ }
+
+ ///
+ /// The which triggered the re-parse.
+ ///
+ public SourceChange SourceChange { get; }
+
+ ///
+ /// The text snapshot used in the re-parse.
+ ///
+ public ITextSnapshot Snapshot { get; }
+
+ ///
+ /// The result of the parsing and code generation.
+ ///
+ public RazorCodeDocument CodeDocument { get; }
+ }
+}
diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/Editor/ForegroundThreadAffinitizedObject.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/Editor/ForegroundThreadAffinitizedObject.cs
new file mode 100644
index 0000000000..720fb6841c
--- /dev/null
+++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/Editor/ForegroundThreadAffinitizedObject.cs
@@ -0,0 +1,34 @@
+// 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.Threading;
+
+namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor
+{
+ internal class ForegroundThreadAffinitizedObject
+ {
+ private readonly Thread _foregroundThread;
+
+ public ForegroundThreadAffinitizedObject()
+ {
+ _foregroundThread = Thread.CurrentThread;
+ }
+
+ public void AssertIsForeground()
+ {
+ if (Thread.CurrentThread != _foregroundThread)
+ {
+ throw new InvalidOperationException("Expected to be on the foreground thread and was not.");
+ }
+ }
+
+ public void AssertIsBackground()
+ {
+ if (Thread.CurrentThread == _foregroundThread)
+ {
+ throw new InvalidOperationException("Expected to be on a background thread and was not.");
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/Editor/RazorSyntaxTreePartialParser.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/Editor/RazorSyntaxTreePartialParser.cs
new file mode 100644
index 0000000000..ade9568907
--- /dev/null
+++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/Editor/RazorSyntaxTreePartialParser.cs
@@ -0,0 +1,69 @@
+// 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 Microsoft.AspNetCore.Razor.Language;
+using Microsoft.AspNetCore.Razor.Language.Legacy;
+using Span = Microsoft.AspNetCore.Razor.Language.Legacy.Span;
+
+namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor
+{
+ internal class RazorSyntaxTreePartialParser
+ {
+ private readonly RazorSyntaxTree _syntaxTree;
+ private Span _lastChangeOwner;
+ private bool _lastResultProvisional;
+
+ public RazorSyntaxTreePartialParser(RazorSyntaxTree syntaxTree)
+ {
+ _syntaxTree = syntaxTree;
+ }
+
+ public PartialParseResultInternal Parse(SourceChange change)
+ {
+ var result = GetPartialParseResult(change);
+
+ // Remember if this was provisionally accepted for next partial parse.
+ _lastResultProvisional = (result & PartialParseResultInternal.Provisional) == PartialParseResultInternal.Provisional;
+
+ return result;
+ }
+
+ private PartialParseResultInternal GetPartialParseResult(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 = _syntaxTree.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);
+ }
+ }
+
+ return result;
+ }
+ }
+}
diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/Editor/VisualStudioRazorParser.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/Editor/VisualStudioRazorParser.cs
new file mode 100644
index 0000000000..94f7935395
--- /dev/null
+++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/Editor/VisualStudioRazorParser.cs
@@ -0,0 +1,181 @@
+// 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.Timers;
+using Microsoft.AspNetCore.Razor.Language;
+using Microsoft.AspNetCore.Razor.Language.Legacy;
+using Microsoft.VisualStudio.Language.Intellisense;
+using Microsoft.VisualStudio.Text;
+using ITextBuffer = Microsoft.VisualStudio.Text.ITextBuffer;
+using Timer = System.Timers.Timer;
+
+namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor
+{
+ internal class VisualStudioRazorParser : IDisposable
+ {
+ // Internal for testing.
+ internal readonly ITextBuffer _textBuffer;
+ internal readonly Timer _idleTimer;
+
+ private const int IdleDelay = 3000;
+ private readonly ICompletionBroker _completionBroker;
+ private readonly BackgroundParser _parser;
+ private readonly ForegroundThreadAffinitizedObject _foregroundThreadAffinitizedObject;
+ private RazorSyntaxTreePartialParser _partialParser;
+
+ public VisualStudioRazorParser(ITextBuffer buffer, RazorTemplateEngine templateEngine, string filePath, ICompletionBroker completionBroker)
+ {
+ if (buffer == null)
+ {
+ throw new ArgumentNullException(nameof(buffer));
+ }
+
+ if (templateEngine == null)
+ {
+ throw new ArgumentNullException(nameof(templateEngine));
+ }
+
+ if (string.IsNullOrEmpty(filePath))
+ {
+ throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(filePath));
+ }
+
+ if (completionBroker == null)
+ {
+ throw new ArgumentNullException(nameof(completionBroker));
+ }
+
+ TemplateEngine = templateEngine;
+ FilePath = filePath;
+ _textBuffer = buffer;
+ _completionBroker = completionBroker;
+ _textBuffer.Changed += TextBuffer_OnChanged;
+ _parser = new BackgroundParser(templateEngine, filePath);
+ _idleTimer = new Timer(IdleDelay);
+ _idleTimer.Elapsed += Onidle;
+ _parser.ResultsReady += OnResultsReady;
+ _foregroundThreadAffinitizedObject = new ForegroundThreadAffinitizedObject();
+
+ _parser.Start();
+ }
+
+ public event EventHandler DocumentStructureChanged;
+
+ public RazorTemplateEngine TemplateEngine { get; }
+
+ public string FilePath { get; }
+
+ public RazorCodeDocument CodeDocument { get; private set; }
+
+ public ITextSnapshot Snapshot { get; private set; }
+
+ public void Reparse()
+ {
+ // Can be called from any thread
+ var snapshot = _textBuffer.CurrentSnapshot;
+ _parser.QueueChange(null, snapshot);
+ }
+
+ public void Dispose()
+ {
+ _foregroundThreadAffinitizedObject.AssertIsForeground();
+
+ _textBuffer.Changed -= TextBuffer_OnChanged;
+ _parser.Dispose();
+ _idleTimer.Dispose();
+ }
+
+ private void TextBuffer_OnChanged(object sender, TextContentChangedEventArgs contentChange)
+ {
+ _foregroundThreadAffinitizedObject.AssertIsForeground();
+
+ if (contentChange.Changes.Count > 0)
+ {
+ // Idle timers are used to track provisional changes. Provisional changes only last for a single text change. After that normal
+ // partial parsing rules apply (stop the timer).
+ _idleTimer.Stop();
+
+
+ var firstChange = contentChange.Changes[0];
+ var lastChange = contentChange.Changes[contentChange.Changes.Count - 1];
+
+ var oldLen = lastChange.OldEnd - firstChange.OldPosition;
+ var newLen = lastChange.NewEnd - firstChange.NewPosition;
+
+ var wasChanged = true;
+ if (oldLen == newLen)
+ {
+ var oldText = contentChange.Before.GetText(firstChange.OldPosition, oldLen);
+ var newText = contentChange.After.GetText(firstChange.NewPosition, newLen);
+ wasChanged = !string.Equals(oldText, newText, StringComparison.Ordinal);
+ }
+
+ if (wasChanged)
+ {
+ var newText = contentChange.After.GetText(firstChange.NewPosition, newLen);
+ var change = new SourceChange(firstChange.OldPosition, oldLen, newText);
+ var snapshot = contentChange.After;
+ var result = PartialParseResultInternal.Rejected;
+
+ using (_parser.SynchronizeMainThreadState())
+ {
+ // Check if we can partial-parse
+ if (_partialParser != null && _parser.IsIdle)
+ {
+ result = _partialParser.Parse(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);
+ }
+
+ if ((result & PartialParseResultInternal.Provisional) == PartialParseResultInternal.Provisional)
+ {
+ _idleTimer.Start();
+ }
+ }
+ }
+ }
+
+ private void Onidle(object sender, ElapsedEventArgs e)
+ {
+ _foregroundThreadAffinitizedObject.AssertIsBackground();
+
+ var textViews = DefaultTextViewRazorDocumentTrackerService.GetTextViews(_textBuffer);
+
+ foreach (var textView in textViews)
+ {
+ if (_completionBroker.IsCompletionActive(textView))
+ {
+ return;
+ }
+ }
+
+ _idleTimer.Stop();
+ Reparse();
+ }
+
+ private void OnResultsReady(object sender, DocumentStructureChangedEventArgs args)
+ {
+ _foregroundThreadAffinitizedObject.AssertIsBackground();
+
+ if (DocumentStructureChanged != null)
+ {
+ if (args.Snapshot != _textBuffer.CurrentSnapshot)
+ {
+ // A different text change is being parsed.
+ return;
+ }
+
+ CodeDocument = args.CodeDocument;
+ Snapshot = args.Snapshot;
+ _partialParser = new RazorSyntaxTreePartialParser(CodeDocument.GetSyntaxTree());
+ DocumentStructureChanged(this, args);
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/DocumentParseCompleteEventArgs.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/Legacy/DocumentParseCompleteEventArgs.cs
similarity index 100%
rename from src/Microsoft.VisualStudio.LanguageServices.Razor/DocumentParseCompleteEventArgs.cs
rename to src/Microsoft.VisualStudio.LanguageServices.Razor/Legacy/DocumentParseCompleteEventArgs.cs
diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/Legacy/RazorEditorParser.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/Legacy/RazorEditorParser.cs
new file mode 100644
index 0000000000..58757df9ef
--- /dev/null
+++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/Legacy/RazorEditorParser.cs
@@ -0,0 +1,598 @@
+// 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
+{
+ 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;
+ FilePath = filePath;
+ _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; }
+
+ public string FilePath { 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");
+ }
+
+ 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; }
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/Microsoft.VisualStudio.LanguageServices.Razor.csproj b/src/Microsoft.VisualStudio.LanguageServices.Razor/Microsoft.VisualStudio.LanguageServices.Razor.csproj
index c351759d2d..65f6f98421 100644
--- a/src/Microsoft.VisualStudio.LanguageServices.Razor/Microsoft.VisualStudio.LanguageServices.Razor.csproj
+++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/Microsoft.VisualStudio.LanguageServices.Razor.csproj
@@ -37,6 +37,7 @@
+
diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/Properties/Resources.Designer.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/Properties/Resources.Designer.cs
index 41d599a8d9..2237686c54 100644
--- a/src/Microsoft.VisualStudio.LanguageServices.Razor/Properties/Resources.Designer.cs
+++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/Properties/Resources.Designer.cs
@@ -10,6 +10,20 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor
private static readonly ResourceManager _resourceManager
= new ResourceManager("Microsoft.VisualStudio.LanguageServices.Razor.Resources", typeof(Resources).GetTypeInfo().Assembly);
+ ///
+ /// Value cannot be null or an empty string.
+ ///
+ internal static string ArgumentCannotBeNullOrEmpty
+ {
+ get => GetString("ArgumentCannotBeNullOrEmpty");
+ }
+
+ ///
+ /// Value cannot be null or an empty string.
+ ///
+ internal static string FormatArgumentCannotBeNullOrEmpty()
+ => GetString("ArgumentCannotBeNullOrEmpty");
+
///
/// An unexpected exception occurred when invoking '{0}.{1}' on the Razor language service.
///
diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/RazorEditorParser.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/RazorEditorParser.cs
deleted file mode 100644
index 4b4a4efe06..0000000000
--- a/src/Microsoft.VisualStudio.LanguageServices.Razor/RazorEditorParser.cs
+++ /dev/null
@@ -1,190 +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;
-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;
- FilePath = filePath;
- _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; }
-
- public string FilePath { 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/Resources.resx b/src/Microsoft.VisualStudio.LanguageServices.Razor/Resources.resx
index 6783beb144..87f559339f 100644
--- a/src/Microsoft.VisualStudio.LanguageServices.Razor/Resources.resx
+++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/Resources.resx
@@ -117,6 +117,9 @@
System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+ Value cannot be null or an empty string.
+
An unexpected exception occurred when invoking '{0}.{1}' on the Razor language service.
diff --git a/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Editor/RazorSyntaxTreePartialParserTest.cs b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Editor/RazorSyntaxTreePartialParserTest.cs
new file mode 100644
index 0000000000..aa11e1dbf2
--- /dev/null
+++ b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Editor/RazorSyntaxTreePartialParserTest.cs
@@ -0,0 +1,765 @@
+// 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 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.Editor
+{
+ public class RazorSyntaxTreePartialParserTest
+ {
+ 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 objectEdit, object expectedDocument)
+ {
+ // Arrange
+ var edit = (TestEdit)objectEdit;
+ var builder = TagHelperDescriptorBuilder.Create("PTagHelper", "TestAssembly");
+ builder.SetTypeName("PTagHelper");
+ builder.TagMatchingRule(rule => rule.TagName = "p");
+ var descriptors = new[]
+ {
+ builder.Build()
+ };
+ var templateEngine = CreateTemplateEngine(tagHelpers: descriptors);
+ var document = TestRazorCodeDocument.Create(
+ TestRazorSourceDocument.Create(edit.OldSnapshot.GetText()),
+ new[] { templateEngine.Options.DefaultImports });
+ templateEngine.Engine.Process(document);
+ var syntaxTree = document.GetSyntaxTree();
+ var parser = new RazorSyntaxTreePartialParser(syntaxTree);
+
+ // Act
+ var result = parser.Parse(edit.Change);
+
+ // Assert
+ Assert.Equal(PartialParseResultInternal.Rejected, result);
+ }
+
+ 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)
+ })),
+ PartialParseResultInternal.Accepted | PartialParseResultInternal.Provisional
+ },
+ {
+ CreateInsertionChange("", 21, "."),
+ new MarkupBlock(
+ new MarkupTagHelperBlock(
+ "p",
+ attributes: new List
+ {
+ new TagHelperAttributeNode(
+ "obj-attr",
+ factory.CodeMarkup("DateTime."),
+ AttributeStructure.SingleQuotes)
+ })),
+ PartialParseResultInternal.Accepted
+ },
+ {
+ CreateInsertionChange("", 25, "."),
+ new MarkupBlock(
+ new MarkupTagHelperBlock(
+ "p",
+ attributes: new List
+ {
+ new TagHelperAttributeNode(
+ "obj-attr",
+ factory.CodeMarkup("1 + DateTime."),
+ AttributeStructure.SingleQuotes)
+ })),
+ PartialParseResultInternal.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),
+ })),
+ PartialParseResultInternal.Accepted | PartialParseResultInternal.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)
+ })),
+ PartialParseResultInternal.Accepted | PartialParseResultInternal.Provisional
+ },
+ };
+ }
+ }
+
+ [Theory]
+ [MemberData(nameof(TagHelperAttributeAcceptData))]
+ public void TagHelperAttributesAreLocatedAndAcceptChangesCorrectly(
+ object editObject,
+ object expectedDocument,
+ object partialParseResultObject)
+ {
+ // Arrange
+ var edit = (TestEdit)editObject;
+ var partialParseResult = (PartialParseResultInternal)partialParseResultObject;
+ 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 templateEngine = CreateTemplateEngine(tagHelpers: descriptors);
+ var document = TestRazorCodeDocument.Create(
+ TestRazorSourceDocument.Create(edit.OldSnapshot.GetText()),
+ new[] { templateEngine.Options.DefaultImports });
+ templateEngine.Engine.Process(document);
+ var syntaxTree = document.GetSyntaxTree();
+ var parser = new RazorSyntaxTreePartialParser(syntaxTree);
+
+ // Act
+ var result = parser.Parse(edit.Change);
+
+ // Assert
+ Assert.Equal(partialParseResult, result);
+ }
+
+ [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: PartialParseResultInternal.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 old = new StringTextSnapshot("foo @date baz");
+ var changed = new StringTextSnapshot("foo @if baz");
+ var edit = new TestEdit(5, 4, old, 2, changed, "if");
+
+ // Act & Assert
+ RunPartialParseRejectionTest(edit);
+ }
+
+ [Fact]
+ public void ImplicitExpressionRejectsWholeIdentifierReplacementToDirective()
+ {
+ // Arrange
+ var old = new StringTextSnapshot("foo @date baz");
+ var changed = new StringTextSnapshot("foo @inherits baz");
+ var edit = new TestEdit(5, 4, old, 8, changed, "inherits");
+
+ // Act & Assert
+ RunPartialParseRejectionTest(edit, PartialParseResultInternal.SpanContextChanged);
+ }
+
+ [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 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: PartialParseResultInternal.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: PartialParseResultInternal.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 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: PartialParseResultInternal.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()));
+ }
+
+ private void RunPartialParseRejectionTest(TestEdit edit, PartialParseResultInternal additionalFlags = 0)
+ {
+ var templateEngine = CreateTemplateEngine();
+ var document = TestRazorCodeDocument.Create(edit.OldSnapshot.GetText());
+ templateEngine.Engine.Process(document);
+ var syntaxTree = document.GetSyntaxTree();
+ var parser = new RazorSyntaxTreePartialParser(syntaxTree);
+
+ var result = parser.Parse(edit.Change);
+ Assert.Equal(PartialParseResultInternal.Rejected | additionalFlags, result);
+ }
+
+ private static void RunPartialParseTest(TestEdit edit, Block expectedTree, PartialParseResultInternal additionalFlags = 0)
+ {
+ var templateEngine = CreateTemplateEngine();
+ var document = TestRazorCodeDocument.Create(edit.OldSnapshot.GetText());
+ templateEngine.Engine.Process(document);
+ var syntaxTree = document.GetSyntaxTree();
+ var parser = new RazorSyntaxTreePartialParser(syntaxTree);
+
+ var result = parser.Parse(edit.Change);
+ Assert.Equal(PartialParseResultInternal.Accepted | additionalFlags, result);
+ ParserTestBase.EvaluateParseTree(expectedTree, syntaxTree.Root);
+ }
+
+ 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 RazorTemplateEngine CreateTemplateEngine(
+ string path = "C:\\This\\Path\\Is\\Just\\For\\Line\\Pragmas.cshtml",
+ 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 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; }
+ }
+ }
+}
diff --git a/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Editor/VisualStudioRazorParserTest.cs b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Editor/VisualStudioRazorParserTest.cs
new file mode 100644
index 0000000000..28ddab5335
--- /dev/null
+++ b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Editor/VisualStudioRazorParserTest.cs
@@ -0,0 +1,892 @@
+// 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.Collections.ObjectModel;
+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.Language.Intellisense;
+using Microsoft.VisualStudio.LanguageServices.Razor.Editor;
+using Microsoft.VisualStudio.Text;
+using Microsoft.VisualStudio.Text.Editor;
+using Microsoft.VisualStudio.Utilities;
+using Xunit;
+
+namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor
+{
+ public class VisualStudioRazorParserTest
+ {
+ private const string TestLinePragmaFileName = "C:\\This\\Path\\Is\\Just\\For\\Line\\Pragmas.cshtml";
+
+ [Fact]
+ public void ConstructorRequiresNonNullPhysicalPath()
+ {
+ Assert.Throws("filePath", () => new VisualStudioRazorParser(new TestTextBuffer(null), CreateTemplateEngine(), null, new TestCompletionBroker()));
+ }
+
+ [Fact]
+ public void ConstructorRequiresNonEmptyPhysicalPath()
+ {
+ Assert.Throws("filePath", () => new VisualStudioRazorParser(new TestTextBuffer(null), CreateTemplateEngine(), string.Empty, new TestCompletionBroker()));
+ }
+
+ [Fact]
+ public void BufferChangeStartsFullReparseIfChangeOverlapsMultipleSpans()
+ {
+ // Arrange
+ var original = new StringTextSnapshot("Foo @bar Baz");
+ var testBuffer = new TestTextBuffer(original);
+ using (var parser = new VisualStudioRazorParser(testBuffer, CreateTemplateEngine(), TestLinePragmaFileName, new TestCompletionBroker()))
+ {
+ parser._idleTimer.Interval = 100;
+ var changed = new StringTextSnapshot("Foo @bap Daz");
+ var edit = new TestEdit(7, 3, original, 3, changed, "p D");
+ var parseComplete = new ManualResetEventSlim();
+ var parseCount = 0;
+ parser.DocumentStructureChanged += (s, a) =>
+ {
+ Interlocked.Increment(ref parseCount);
+ parseComplete.Set();
+ };
+
+ // Act - 1
+ testBuffer.ApplyEdit(edit);
+ DoWithTimeoutIfNotDebugging(parseComplete.Wait); // Wait for the parse to finish
+
+ // Assert - 1
+ Assert.Equal(1, parseCount);
+ parseComplete.Reset();
+
+ // Act - 2
+ testBuffer.ApplyEdit(edit);
+
+ // Assert - 2
+ DoWithTimeoutIfNotDebugging(parseComplete.Wait);
+ Assert.Equal(2, parseCount);
+ }
+ }
+
+ [Fact]
+ public void AwaitPeriodInsertionAcceptedProvisionally()
+ {
+ // Arrange
+ var original = new StringTextSnapshot("foo @await Html baz");
+ using (var manager = CreateParserManager(original))
+ {
+ var factory = new SpanFactory();
+ var changed = new StringTextSnapshot("foo @await Html. baz");
+ var edit = new TestEdit(15, 0, original, 1, changed, ".");
+ manager.InitializeWithDocument(edit.OldSnapshot);
+
+ // Act
+ manager.ApplyEditAndWaitForReparse(edit);
+
+ // Assert
+ Assert.Equal(2, manager.ParseCount);
+ ParserTestBase.EvaluateParseTree(manager.CurrentSyntaxTree.Root, new MarkupBlock(
+ factory.Markup("foo "),
+ new ExpressionBlock(
+ factory.CodeTransition(),
+ factory.Code("await Html").AsImplicitExpression(CSharpCodeParser.DefaultKeywords).Accepts(AcceptedCharactersInternal.WhiteSpace | AcceptedCharactersInternal.NonWhiteSpace)),
+ factory.Markup(". baz")));
+ }
+ }
+
+ [Fact]
+ public void ImplicitExpressionAcceptsDotlessCommitInsertionsInStatementBlockAfterIdentifiers()
+ {
+ var factory = new SpanFactory();
+ var changed = new StringTextSnapshot("@{" + Environment.NewLine
+ + " @DateTime." + Environment.NewLine
+ + "}");
+ var original = new StringTextSnapshot("@{" + Environment.NewLine
+ + " @DateTime" + Environment.NewLine
+ + "}");
+
+ var edit = new TestEdit(15 + Environment.NewLine.Length, 0, original, 1, changed, ".");
+ using (var manager = CreateParserManager(original))
+ {
+ void ApplyAndVerifyPartialChange(TestEdit testEdit, string expectedCode)
+ {
+ manager.ApplyEdit(testEdit);
+ Assert.Equal(1, manager.ParseCount);
+ ParserTestBase.EvaluateParseTree(manager.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, "DateTime.");
+
+ original = changed;
+ changed = new StringTextSnapshot("@{" + Environment.NewLine
+ + " @DateTime.." + Environment.NewLine
+ + "}");
+ edit = new TestEdit(16 + Environment.NewLine.Length, 0, original, 1, changed, ".");
+
+ ApplyAndVerifyPartialChange(edit, "DateTime..");
+
+ original = changed;
+ changed = new StringTextSnapshot("@{" + Environment.NewLine
+ + " @DateTime.Now." + Environment.NewLine
+ + "}");
+ edit = new TestEdit(16 + Environment.NewLine.Length, 0, original, 3, changed, "Now");
+
+ ApplyAndVerifyPartialChange(edit, "DateTime.Now.");
+ }
+ }
+
+ [Fact]
+ public void ImplicitExpressionAcceptsDotlessCommitInsertionsInStatementBlock()
+ {
+ var factory = new SpanFactory();
+ var changed = new StringTextSnapshot("@{" + Environment.NewLine
+ + " @DateT." + Environment.NewLine
+ + "}");
+ var original = new StringTextSnapshot("@{" + Environment.NewLine
+ + " @DateT" + Environment.NewLine
+ + "}");
+
+ var edit = new TestEdit(12 + Environment.NewLine.Length, 0, original, 1, changed, ".");
+ using (var manager = CreateParserManager(original))
+ {
+ void ApplyAndVerifyPartialChange(TestEdit testEdit, string expectedCode)
+ {
+ manager.ApplyEdit(testEdit);
+ Assert.Equal(1, manager.ParseCount);
+ ParserTestBase.EvaluateParseTree(manager.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, "DateT.");
+
+ original = changed;
+ changed = new StringTextSnapshot("@{" + Environment.NewLine
+ + " @DateTime." + Environment.NewLine
+ + "}");
+ edit = new TestEdit(12 + Environment.NewLine.Length, 0, original, 3, changed, "ime");
+
+ ApplyAndVerifyPartialChange(edit, "DateTime.");
+ }
+ }
+
+ [Fact]
+ public void ImplicitExpressionProvisionallyAcceptsDotlessCommitInsertions()
+ {
+ var factory = new SpanFactory();
+ var changed = new StringTextSnapshot("foo @DateT. baz");
+ var original = new StringTextSnapshot("foo @DateT baz");
+ var edit = new TestEdit(10, 0, original, 1, changed, ".");
+ using (var manager = CreateParserManager(original, idleDelay: 250))
+ {
+ void ApplyAndVerifyPartialChange(TestEdit testEdit, string expectedCode)
+ {
+ manager.ApplyEdit(testEdit);
+ Assert.Equal(1, manager.ParseCount);
+
+ ParserTestBase.EvaluateParseTree(manager.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, "DateT.");
+
+ original = changed;
+ changed = new StringTextSnapshot("foo @DateTime. baz");
+ edit = new TestEdit(10, 0, original, 3, changed, "ime");
+
+ ApplyAndVerifyPartialChange(edit, "DateTime.");
+
+ // Verify the reparse finally comes
+ manager.WaitForReparse();
+
+ Assert.Equal(2, manager.ParseCount);
+ ParserTestBase.EvaluateParseTree(manager.CurrentSyntaxTree.Root, new MarkupBlock(
+ factory.Markup("foo "),
+ new ExpressionBlock(
+ factory.CodeTransition(),
+ factory.Code("DateTime").AsImplicitExpression(CSharpCodeParser.DefaultKeywords).Accepts(AcceptedCharactersInternal.NonWhiteSpace)),
+ factory.Markup(". baz")));
+ }
+ }
+
+ [Fact]
+ public void ImplicitExpressionProvisionallyAcceptsDotlessCommitInsertionsAfterIdentifiers()
+ {
+ var factory = new SpanFactory();
+ var changed = new StringTextSnapshot("foo @DateTime. baz");
+ var original = new StringTextSnapshot("foo @DateTime baz");
+ var edit = new TestEdit(13, 0, original, 1, changed, ".");
+ using (var manager = CreateParserManager(original, idleDelay: 250))
+ {
+ void ApplyAndVerifyPartialChange(TestEdit testEdit, string expectedCode)
+ {
+ manager.ApplyEdit(testEdit);
+ Assert.Equal(1, manager.ParseCount);
+
+ ParserTestBase.EvaluateParseTree(manager.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, "DateTime.");
+
+ original = changed;
+ changed = new StringTextSnapshot("foo @DateTime.. baz");
+ edit = new TestEdit(14, 0, original, 1, changed, ".");
+
+ ApplyAndVerifyPartialChange(edit, "DateTime..");
+
+ original = changed;
+ changed = new StringTextSnapshot("foo @DateTime.Now. baz");
+ edit = new TestEdit(14, 0, original, 3, changed, "Now");
+
+ ApplyAndVerifyPartialChange(edit, "DateTime.Now.");
+
+ // Verify the reparse eventually happens
+ manager.WaitForReparse();
+
+ Assert.Equal(2, manager.ParseCount);
+ ParserTestBase.EvaluateParseTree(manager.CurrentSyntaxTree.Root, 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 ImplicitExpressionProvisionallyAcceptsCaseInsensitiveDotlessCommitInsertions_NewRoslynIntegration()
+ {
+ var factory = new SpanFactory();
+ var original = new StringTextSnapshot("foo @date baz");
+ var changed = new StringTextSnapshot("foo @date. baz");
+ var edit = new TestEdit(9, 0, original, 1, changed, ".");
+ using (var manager = CreateParserManager(original, idleDelay: 250))
+ {
+ void ApplyAndVerifyPartialChange(Action applyEdit, string expectedCode)
+ {
+ applyEdit();
+ Assert.Equal(1, manager.ParseCount);
+
+ ParserTestBase.EvaluateParseTree(manager.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(() => manager.ApplyEdit(edit), "date.");
+
+ original = changed;
+ changed = new StringTextSnapshot("foo @date baz");
+ edit = new TestEdit(9, 1, original, 0, changed, "");
+
+ // @date. => @date
+ ApplyAndVerifyPartialChange(() => manager.ApplyEdit(edit), "date");
+
+ original = changed;
+ changed = new StringTextSnapshot("foo @DateTime baz");
+ edit = new TestEdit(5, 4, original, 8, changed, "DateTime");
+
+ // @date => @DateTime
+ ApplyAndVerifyPartialChange(() => manager.ApplyEdit(edit), "DateTime");
+
+ original = changed;
+ changed = new StringTextSnapshot("foo @DateTime. baz");
+ edit = new TestEdit(13, 0, original, 1, changed, ".");
+
+ // @DateTime => @DateTime.
+ ApplyAndVerifyPartialChange(() => manager.ApplyEdit(edit), "DateTime.");
+
+ // Verify the reparse eventually happens
+ manager.WaitForReparse();
+
+ Assert.Equal(2, manager.ParseCount);
+ ParserTestBase.EvaluateParseTree(manager.CurrentSyntaxTree.Root, new MarkupBlock(
+ factory.Markup("foo "),
+ new ExpressionBlock(
+ factory.CodeTransition(),
+ factory.Code("DateTime").AsImplicitExpression(CSharpCodeParser.DefaultKeywords).Accepts(AcceptedCharactersInternal.NonWhiteSpace)),
+ factory.Markup(". baz")));
+ }
+ }
+
+ [Fact]
+ public void ImplicitExpressionRejectsChangeWhichWouldHaveBeenAcceptedIfLastChangeWasProvisionallyAcceptedOnDifferentSpan()
+ {
+ // Arrange
+ var factory = new SpanFactory();
+ 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(dotTyped.OldSnapshot))
+ {
+ manager.InitializeWithDocument(dotTyped.OldSnapshot);
+
+ // Apply the dot change
+ manager.ApplyEditAndWaitForReparse(dotTyped);
+
+ // Act (apply the identifier start char change)
+ manager.ApplyEditAndWaitForParse(charTyped);
+
+ // Assert
+ Assert.Equal(2, manager.ParseCount);
+ ParserTestBase.EvaluateParseTree(manager.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()
+ {
+ // Arrange
+ var factory = new SpanFactory();
+ 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(dotTyped.OldSnapshot, idleDelay: 250))
+ {
+ manager.InitializeWithDocument(dotTyped.OldSnapshot);
+
+ // Apply the dot change
+ manager.ApplyEdit(dotTyped);
+
+ // Act (apply the identifier start char change)
+ manager.ApplyEdit(charTyped);
+
+ // Assert
+ Assert.Equal(1, manager.ParseCount);
+ ParserTestBase.EvaluateParseTree(manager.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 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 TestParserManager CreateParserManager(ITextSnapshot originalSnapshot, int idleDelay = 50)
+ {
+ var parser = new VisualStudioRazorParser(new TestTextBuffer(originalSnapshot), CreateTemplateEngine(), TestLinePragmaFileName, new TestCompletionBroker());
+
+ // Normal idle delay is 3000 milliseconds, for testing we want it to be far shorter.
+ parser._idleTimer.Interval = idleDelay;
+ 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)
+ {
+ // Arrange
+ 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
+ };
+ using (var manager = CreateParserManager(old))
+ {
+ manager.InitializeWithDocument(edit.OldSnapshot);
+
+ // Act
+ manager.ApplyEditAndWaitForParse(edit);
+
+ // Assert
+ Assert.Equal(2, manager.ParseCount);
+ }
+ }
+
+ 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;
+ private readonly ManualResetEventSlim _reparseComplete;
+ private readonly TestTextBuffer _testBuffer;
+ private readonly VisualStudioRazorParser _parser;
+
+ public TestParserManager(VisualStudioRazorParser parser)
+ {
+ _parserComplete = new ManualResetEventSlim();
+ _reparseComplete = new ManualResetEventSlim();
+ _testBuffer = (TestTextBuffer)parser._textBuffer;
+ ParseCount = 0;
+ _parser = parser;
+ parser.DocumentStructureChanged += (sender, args) =>
+ {
+ Interlocked.Increment(ref ParseCount);
+ _parserComplete.Set();
+
+ if (args.SourceChange == null)
+ {
+ // Reparse occurred
+ _reparseComplete.Set();
+ }
+
+ CurrentSyntaxTree = args.CodeDocument.GetSyntaxTree();
+ };
+ }
+
+ public RazorSyntaxTree CurrentSyntaxTree { get; private set; }
+
+ public void InitializeWithDocument(ITextSnapshot snapshot)
+ {
+ var old = new StringTextSnapshot(string.Empty);
+ var initialChange = new SourceChange(0, 0, snapshot.GetText());
+ var edit = new TestEdit
+ {
+ Change = initialChange,
+ OldSnapshot = old,
+ NewSnapshot = snapshot
+ };
+ ApplyEditAndWaitForParse(edit);
+ }
+
+ public void ApplyEdit(TestEdit edit)
+ {
+ _testBuffer.ApplyEdit(edit);
+ }
+
+ public void ApplyEditAndWaitForParse(TestEdit edit)
+ {
+ ApplyEdit(edit);
+ WaitForParse();
+ }
+
+ public void ApplyEditAndWaitForReparse(TestEdit edit)
+ {
+ ApplyEdit(edit);
+ WaitForReparse();
+ }
+
+ public void WaitForParse()
+ {
+ DoWithTimeoutIfNotDebugging(_parserComplete.Wait); // Wait for the parse to finish
+ _parserComplete.Reset();
+ }
+
+ public void WaitForReparse()
+ {
+ DoWithTimeoutIfNotDebugging(_reparseComplete.Wait);
+ _reparseComplete.Reset();
+ }
+
+ public void Dispose()
+ {
+ _parser.Dispose();
+ }
+ }
+
+ private class TextChange : ITextChange
+ {
+ public TextChange(TestEdit edit) : this(edit.Change)
+ {
+ }
+
+ public TextChange(SourceChange change)
+ {
+ var changeSpan = change.Span;
+
+ OldPosition = changeSpan.AbsoluteIndex;
+ NewPosition = OldPosition;
+ OldEnd = changeSpan.AbsoluteIndex + changeSpan.Length;
+ NewEnd = changeSpan.AbsoluteIndex + change.NewText.Length;
+ }
+
+ public Text.Span OldSpan => throw new NotImplementedException();
+
+ public Text.Span NewSpan => throw new NotImplementedException();
+
+ public int OldPosition { get; }
+
+ public int NewPosition { get; }
+
+ public int Delta => throw new NotImplementedException();
+
+ public int OldEnd { get; }
+
+ public int NewEnd { get; }
+
+ public string OldText => throw new NotImplementedException();
+
+ public string NewText => throw new NotImplementedException();
+
+ public int OldLength => throw new NotImplementedException();
+
+ public int NewLength => throw new NotImplementedException();
+
+ public int LineCountDelta => throw new NotImplementedException();
+ }
+
+ private class TestCompletionBroker : ICompletionBroker
+ {
+ public ICompletionSession CreateCompletionSession(ITextView textView, ITrackingPoint triggerPoint, bool trackCaret)
+ {
+ throw new NotImplementedException();
+ }
+
+ public void DismissAllSessions(ITextView textView)
+ {
+ throw new NotImplementedException();
+ }
+
+ public ReadOnlyCollection GetSessions(ITextView textView)
+ {
+ throw new NotImplementedException();
+ }
+
+ public bool IsCompletionActive(ITextView textView)
+ {
+ return false;
+ }
+
+ public ICompletionSession TriggerCompletion(ITextView textView)
+ {
+ throw new NotImplementedException();
+ }
+
+ public ICompletionSession TriggerCompletion(ITextView textView, ITrackingPoint triggerPoint, bool trackCaret)
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ private class TestTextBuffer : Text.ITextBuffer
+ {
+ private ITextSnapshot _currentSnapshot;
+
+ public TestTextBuffer(ITextSnapshot initialSnapshot)
+ {
+ _currentSnapshot = initialSnapshot;
+ ReadOnlyRegionsChanged += (sender, args) => { };
+ ChangedLowPriority += (sender, args) => { };
+ ChangedHighPriority += (sender, args) => { };
+ Changing += (sender, args) => { };
+ PostChanged += (sender, args) => { };
+ ContentTypeChanged += (sender, args) => { };
+ }
+
+ public void ApplyEdit(TestEdit edit)
+ {
+ var args = new TextContentChangedEventArgs(edit.OldSnapshot, edit.NewSnapshot, new EditOptions(), null);
+ args.Changes.Add(new TextChange(edit));
+ Changed?.Invoke(this, args);
+
+ ReadOnlyRegionsChanged?.Invoke(null, null);
+ ChangedLowPriority?.Invoke(null, null);
+ ChangedHighPriority?.Invoke(null, null);
+ Changing?.Invoke(null, null);
+ PostChanged?.Invoke(null, null);
+ ContentTypeChanged?.Invoke(null, null);
+
+ _currentSnapshot = edit.NewSnapshot;
+ }
+
+ public IContentType ContentType => throw new NotImplementedException();
+
+ public ITextSnapshot CurrentSnapshot => _currentSnapshot;
+
+ public bool EditInProgress => throw new NotImplementedException();
+
+ public PropertyCollection Properties => throw new NotImplementedException();
+
+ public event EventHandler ReadOnlyRegionsChanged;
+ public event EventHandler Changed;
+ public event EventHandler ChangedLowPriority;
+ public event EventHandler ChangedHighPriority;
+ public event EventHandler Changing;
+ public event EventHandler PostChanged;
+ public event EventHandler ContentTypeChanged;
+
+ public void ChangeContentType(IContentType newContentType, object editTag)
+ {
+ throw new NotImplementedException();
+ }
+
+ public bool CheckEditAccess()
+ {
+ throw new NotImplementedException();
+ }
+
+ public ITextEdit CreateEdit(EditOptions options, int? reiteratedVersionNumber, object editTag)
+ {
+ throw new NotImplementedException();
+ }
+
+ public ITextEdit CreateEdit()
+ {
+ throw new NotImplementedException();
+ }
+
+ public IReadOnlyRegionEdit CreateReadOnlyRegionEdit()
+ {
+ throw new NotImplementedException();
+ }
+
+ public ITextSnapshot Delete(Text.Span deleteSpan)
+ {
+ throw new NotImplementedException();
+ }
+
+ public NormalizedSpanCollection GetReadOnlyExtents(Text.Span span)
+ {
+ throw new NotImplementedException();
+ }
+
+ public ITextSnapshot Insert(int position, string text)
+ {
+ throw new NotImplementedException();
+ }
+
+ public bool IsReadOnly(int position)
+ {
+ throw new NotImplementedException();
+ }
+
+ public bool IsReadOnly(int position, bool isEdit)
+ {
+ throw new NotImplementedException();
+ }
+
+ public bool IsReadOnly(Text.Span span)
+ {
+ throw new NotImplementedException();
+ }
+
+ public bool IsReadOnly(Text.Span span, bool isEdit)
+ {
+ throw new NotImplementedException();
+ }
+
+ public ITextSnapshot Replace(Text.Span replaceSpan, string replaceWith)
+ {
+ throw new NotImplementedException();
+ }
+
+ public void TakeThreadOwnership()
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ 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/RazorEditorParserTest.cs b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Legacy/RazorEditorParserTest.cs
similarity index 99%
rename from test/Microsoft.VisualStudio.LanguageServices.Razor.Test/RazorEditorParserTest.cs
rename to test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Legacy/RazorEditorParserTest.cs
index 5fd369d42a..d78d8b6807 100644
--- a/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/RazorEditorParserTest.cs
+++ b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Legacy/RazorEditorParserTest.cs
@@ -323,7 +323,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor
modified.LinkNodes();
// Act
- var treesAreDifferent = BackgroundParser.TreesAreDifferent(
+ var treesAreDifferent = RazorEditorParser.BackgroundParser.TreesAreDifferent(
original,
modified,
new[]
@@ -355,7 +355,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor
factory.Code("f")
.AsImplicitExpression(CSharpCodeParser.DefaultKeywords, acceptTrailingDot: false)),
factory.Markup("
"));
- Assert.True(BackgroundParser.TreesAreDifferent(
+ Assert.True(RazorEditorParser.BackgroundParser.TreesAreDifferent(
original,
modified,
new[]
@@ -386,7 +386,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor
factory.Markup(""));
original.LinkNodes();
modified.LinkNodes();
- Assert.False(BackgroundParser.TreesAreDifferent(
+ Assert.False(RazorEditorParser.BackgroundParser.TreesAreDifferent(
original,
modified,
new[]
diff --git a/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/StringTextSnapshot.cs b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/StringTextSnapshot.cs
index c6e183029b..0cbcc181d4 100644
--- a/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/StringTextSnapshot.cs
+++ b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/StringTextSnapshot.cs
@@ -11,96 +11,91 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy
{
public class StringTextSnapshot : ITextSnapshot
{
- private readonly string _content;
-
public StringTextSnapshot(string content)
{
- _content = content;
+ Content = content;
}
- public char this[int position] => _content[position];
+ public string Content { get; }
+
+ public char this[int position] => Content[position];
+
+ public ITextVersion Version { get; } = new TextVersion();
+
+ public int Length => Content.Length;
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)
+ public void CopyTo(int sourceIndex, char[] destination, int destinationIndex, int count) => Content.CopyTo(sourceIndex, destination, destinationIndex, count);
+
+ 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 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 void Write(TextWriter writer, VisualStudio.Text.Span span) => throw new NotImplementedException();
+
+ public void Write(TextWriter writer) => throw new NotImplementedException();
+
+ private class TextVersion : ITextVersion
{
- _content.CopyTo(sourceIndex, destination, destinationIndex, count);
- }
+ public INormalizedTextChangeCollection Changes { get; } = new TextChangeCollection();
- public ITrackingPoint CreateTrackingPoint(int position, PointTrackingMode trackingMode)
- {
- throw new NotImplementedException();
- }
+ public ITextVersion Next => throw new NotImplementedException();
- public ITrackingPoint CreateTrackingPoint(int position, PointTrackingMode trackingMode, TrackingFidelityMode trackingFidelity)
- {
- throw new NotImplementedException();
- }
+ public int Length => throw new NotImplementedException();
- public ITrackingSpan CreateTrackingSpan(VisualStudio.Text.Span span, SpanTrackingMode trackingMode)
- {
- throw new NotImplementedException();
- }
+ public VisualStudio.Text.ITextBuffer TextBuffer => throw new NotImplementedException();
- public ITrackingSpan CreateTrackingSpan(VisualStudio.Text.Span span, SpanTrackingMode trackingMode, TrackingFidelityMode trackingFidelity)
- {
- throw new NotImplementedException();
- }
+ public int VersionNumber => throw new NotImplementedException();
- public ITrackingSpan CreateTrackingSpan(int start, int length, SpanTrackingMode trackingMode)
- {
- throw new NotImplementedException();
- }
+ public int ReiteratedVersionNumber => throw new NotImplementedException();
- public ITrackingSpan CreateTrackingSpan(int start, int length, SpanTrackingMode trackingMode, TrackingFidelityMode trackingFidelity)
- {
- throw new NotImplementedException();
- }
+ public ITrackingSpan CreateCustomTrackingSpan(VisualStudio.Text.Span span, TrackingFidelityMode trackingFidelity, object customState, CustomTrackToVersion behavior) => throw new NotImplementedException();
- public ITextSnapshotLine GetLineFromLineNumber(int lineNumber)
- {
- throw new NotImplementedException();
- }
+ public ITrackingPoint CreateTrackingPoint(int position, PointTrackingMode trackingMode) => throw new NotImplementedException();
- public ITextSnapshotLine GetLineFromPosition(int position)
- {
- throw new NotImplementedException();
- }
+ public ITrackingPoint CreateTrackingPoint(int position, PointTrackingMode trackingMode, TrackingFidelityMode trackingFidelity) => throw new NotImplementedException();
- public int GetLineNumberFromPosition(int position)
- {
- throw new NotImplementedException();
- }
+ public ITrackingSpan CreateTrackingSpan(VisualStudio.Text.Span span, SpanTrackingMode trackingMode) => throw new NotImplementedException();
- public string GetText(VisualStudio.Text.Span span)
- {
- throw new NotImplementedException();
- }
+ public ITrackingSpan CreateTrackingSpan(VisualStudio.Text.Span span, SpanTrackingMode trackingMode, TrackingFidelityMode trackingFidelity) => throw new NotImplementedException();
- public string GetText(int startIndex, int length) => _content.Substring(startIndex, length);
+ public ITrackingSpan CreateTrackingSpan(int start, int length, SpanTrackingMode trackingMode) => throw new NotImplementedException();
- public string GetText() => _content;
+ public ITrackingSpan CreateTrackingSpan(int start, int length, SpanTrackingMode trackingMode, TrackingFidelityMode trackingFidelity) => throw new NotImplementedException();
- 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();
+ private class TextChangeCollection : List, INormalizedTextChangeCollection
+ {
+ public bool IncludesLineChanges => false;
+ }
}
}
}