Made the Visual Studio parser smarter about overlapping reparse requests.

- Prior to this the parser would think that a non-latest reparse request was the latest because the only way we would check to see if a change reference was "latest" would be to do an equality check on the snapshot and `SourceChange`; the issue here was that `SourceChange` was null but the snapshots were the same. Problems could arise with this due to project context changes.
- Added tests to validate the new reparse behavior.
- Renamed `Edit` in the `BackgroundParser` to `ChangeReference` also refactored all the "edit" text in `BackgroundParser` to be `ChangerReference` like.
- Added a new event args to be the DTO between the internal and external parser implementations. This is how we could pas additional information in order to determine "latest" change reference.

#2336
This commit is contained in:
N. Taylor Mullen 2018-05-08 16:46:07 -07:00
parent a412299881
commit 7c70207594
4 changed files with 109 additions and 67 deletions

View File

@ -30,7 +30,7 @@ namespace Microsoft.VisualStudio.Editor.Razor
/// <summary>
/// Fired on the main thread.
/// </summary>
public event EventHandler<DocumentStructureChangedEventArgs> ResultsReady;
public event EventHandler<BackgroundParserResultsReadyEventArgs> ResultsReady;
public bool IsIdle
{
@ -47,10 +47,11 @@ namespace Microsoft.VisualStudio.Editor.Razor
_main.Cancel();
}
public void QueueChange(SourceChange change, ITextSnapshot snapshot)
public ChangeReference QueueChange(SourceChange change, ITextSnapshot snapshot)
{
var edit = new Edit(change, snapshot);
_main.QueueChange(edit);
var changeReference = new ChangeReference(change, snapshot);
_main.QueueChange(changeReference);
return changeReference;
}
public void Dispose()
@ -63,7 +64,7 @@ namespace Microsoft.VisualStudio.Editor.Razor
return _main.Lock();
}
protected virtual void OnResultsReady(DocumentStructureChangedEventArgs args)
protected virtual void OnResultsReady(BackgroundParserResultsReadyEventArgs args)
{
using (SynchronizeMainThreadState())
{
@ -115,7 +116,7 @@ namespace Microsoft.VisualStudio.Editor.Razor
private string _fileName;
private readonly object _stateLock = new object();
private IList<Edit> _changes = new List<Edit>();
private IList<ChangeReference> _changes = new List<ChangeReference>();
public MainThreadState(string fileName)
{
@ -124,7 +125,7 @@ namespace Microsoft.VisualStudio.Editor.Razor
SetThreadId(Thread.CurrentThread.ManagedThreadId);
}
public event EventHandler<DocumentStructureChangedEventArgs> ResultsReady;
public event EventHandler<BackgroundParserResultsReadyEventArgs> ResultsReady;
public CancellationToken CancelToken
{
@ -154,7 +155,7 @@ namespace Microsoft.VisualStudio.Editor.Razor
return new DisposableAction(() => Monitor.Exit(_stateLock));
}
public void QueueChange(Edit edit)
public void QueueChange(ChangeReference change)
{
// Any thread can queue a change.
@ -166,7 +167,7 @@ namespace Microsoft.VisualStudio.Editor.Razor
_currentParcelCancelSource.Cancel();
}
_changes.Add(edit);
_changes.Add(change);
_hasParcel.Set();
}
}
@ -182,12 +183,12 @@ namespace Microsoft.VisualStudio.Editor.Razor
_currentParcelCancelSource = new CancellationTokenSource();
var changes = _changes;
_changes = new List<Edit>();
_changes = new List<ChangeReference>();
return new WorkParcel(changes, _currentParcelCancelSource.Token);
}
}
public void ReturnParcel(DocumentStructureChangedEventArgs args)
public void ReturnParcel(BackgroundParserResultsReadyEventArgs args)
{
lock (_stateLock)
{
@ -242,7 +243,7 @@ namespace Microsoft.VisualStudio.Editor.Razor
private CancellationToken _shutdownToken;
private RazorProjectEngine _projectEngine;
private RazorSyntaxTree _currentSyntaxTree;
private IList<Edit> _previouslyDiscarded = new List<Edit>();
private IList<ChangeReference> _previouslyDiscarded = new List<ChangeReference>();
public BackgroundThread(MainThreadState main, RazorProjectEngine projectEngine, string filePath, string projectDirectory)
{
@ -274,30 +275,30 @@ namespace Microsoft.VisualStudio.Editor.Razor
{
// Grab the parcel of work to do
var parcel = _main.GetParcel();
if (parcel.Edits.Any())
if (parcel.Changes.Any())
{
try
{
DocumentStructureChangedEventArgs args = null;
BackgroundParserResultsReadyEventArgs args = null;
using (var linkedCancel = CancellationTokenSource.CreateLinkedTokenSource(_shutdownToken, parcel.CancelToken))
{
if (!linkedCancel.IsCancellationRequested)
{
// Collect ALL changes
List<Edit> allEdits;
List<ChangeReference> allChanges;
if (_previouslyDiscarded != null)
{
allEdits = Enumerable.Concat(_previouslyDiscarded, parcel.Edits).ToList();
allChanges = Enumerable.Concat(_previouslyDiscarded, parcel.Changes).ToList();
}
else
{
allEdits = parcel.Edits.ToList();
allChanges = parcel.Changes.ToList();
}
var finalEdit = allEdits.Last();
var finalChange = allChanges.Last();
var results = ParseChange(finalEdit.Snapshot, linkedCancel.Token);
var results = ParseChange(finalChange.Snapshot, linkedCancel.Token);
if (results != null && !linkedCancel.IsCancellationRequested)
{
@ -307,15 +308,12 @@ namespace Microsoft.VisualStudio.Editor.Razor
_currentSyntaxTree = results.GetSyntaxTree();
// Build Arguments
args = new DocumentStructureChangedEventArgs(
finalEdit.Change,
finalEdit.Snapshot,
results);
args = new BackgroundParserResultsReadyEventArgs(finalChange, results);
}
else
{
// Parse completed but we were cancelled in the mean time. Add these to the discarded changes set
_previouslyDiscarded = allEdits;
_previouslyDiscarded = allChanges;
}
}
}
@ -378,20 +376,20 @@ namespace Microsoft.VisualStudio.Editor.Razor
private class WorkParcel
{
public WorkParcel(IList<Edit> changes, CancellationToken cancelToken)
public WorkParcel(IList<ChangeReference> changes, CancellationToken cancelToken)
{
Edits = changes;
Changes = changes;
CancelToken = cancelToken;
}
public CancellationToken CancelToken { get; }
public IList<Edit> Edits { get; }
public IList<ChangeReference> Changes { get; }
}
private class Edit
internal class ChangeReference
{
public Edit(SourceChange change, ITextSnapshot snapshot)
public ChangeReference(SourceChange change, ITextSnapshot snapshot)
{
Change = change;
Snapshot = snapshot;
@ -399,7 +397,7 @@ namespace Microsoft.VisualStudio.Editor.Razor
public SourceChange Change { get; }
public ITextSnapshot Snapshot { get; set; }
public ITextSnapshot Snapshot { get; }
}
}
}

View File

@ -0,0 +1,22 @@
// 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 static Microsoft.VisualStudio.Editor.Razor.BackgroundParser;
namespace Microsoft.VisualStudio.Editor.Razor
{
internal class BackgroundParserResultsReadyEventArgs : EventArgs
{
public BackgroundParserResultsReadyEventArgs(ChangeReference edit, RazorCodeDocument codeDocument)
{
ChangeReference = edit;
CodeDocument = codeDocument;
}
public ChangeReference ChangeReference { get; }
public RazorCodeDocument CodeDocument { get; }
}
}

View File

@ -12,6 +12,7 @@ using Microsoft.AspNetCore.Razor.Language.Legacy;
using Microsoft.CodeAnalysis.Razor;
using Microsoft.CodeAnalysis.Razor.Editor;
using Microsoft.VisualStudio.Text;
using static Microsoft.VisualStudio.Editor.Razor.BackgroundParser;
using ITextBuffer = Microsoft.VisualStudio.Text.ITextBuffer;
using Timer = System.Threading.Timer;
@ -317,8 +318,7 @@ namespace Microsoft.VisualStudio.Editor.Razor
{
_dispatcher.AssertForegroundThread();
_latestChangeReference = new ChangeReference(change, snapshot);
_parser.QueueChange(change, snapshot);
_latestChangeReference = _parser.QueueChange(change, snapshot);
}
private void OnNotifyForegroundIdle()
@ -357,7 +357,7 @@ namespace Microsoft.VisualStudio.Editor.Razor
}
}
private void OnResultsReady(object sender, DocumentStructureChangedEventArgs args)
private void OnResultsReady(object sender, BackgroundParserResultsReadyEventArgs args)
{
_dispatcher.AssertBackgroundThread();
@ -375,21 +375,25 @@ namespace Microsoft.VisualStudio.Editor.Razor
return;
}
var args = (DocumentStructureChangedEventArgs)state;
var backgroundParserArgs = (BackgroundParserResultsReadyEventArgs)state;
if (_latestChangeReference == null || // extra hardening
!_latestChangeReference.IsAssociatedWith(args) ||
args.Snapshot != TextBuffer.CurrentSnapshot)
_latestChangeReference != backgroundParserArgs.ChangeReference ||
backgroundParserArgs.ChangeReference.Snapshot != TextBuffer.CurrentSnapshot)
{
// In the middle of parsing a newer change or about to parse a newer change.
return;
}
_latestChangeReference = null;
_codeDocument = args.CodeDocument;
_snapshot = args.Snapshot;
_codeDocument = backgroundParserArgs.CodeDocument;
_snapshot = backgroundParserArgs.ChangeReference.Snapshot;
_partialParser = new RazorSyntaxTreePartialParser(CodeDocument.GetSyntaxTree());
DocumentStructureChanged?.Invoke(this, args);
var documentStructureChangedArgs = new DocumentStructureChangedEventArgs(
backgroundParserArgs.ChangeReference.Change,
backgroundParserArgs.ChangeReference.Snapshot,
backgroundParserArgs.CodeDocument);
DocumentStructureChanged?.Invoke(this, documentStructureChangedArgs);
}
private void ConfigureProjectEngine(RazorProjectEngineBuilder builder)
@ -432,25 +436,5 @@ namespace Microsoft.VisualStudio.Editor.Razor
return _tagHelpers;
}
}
// Internal for testing
internal class ChangeReference
{
public ChangeReference(SourceChange change, ITextSnapshot snapshot)
{
Change = change;
Snapshot = snapshot;
}
public SourceChange Change { get; }
public ITextSnapshot Snapshot { get; }
public bool IsAssociatedWith(DocumentStructureChangedEventArgs other)
{
return Change == other.SourceChange &&
Snapshot == other.Snapshot;
}
}
}
}

View File

@ -108,10 +108,9 @@ namespace Microsoft.VisualStudio.Editor.Razor
{
var called = false;
parser.DocumentStructureChanged += (sender, e) => called = true;
parser._latestChangeReference = new DefaultVisualStudioRazorParser.ChangeReference(null, new StringTextSnapshot(string.Empty));
var args = new DocumentStructureChangedEventArgs(
new SourceChange(0, 0, string.Empty),
new StringTextSnapshot(string.Empty),
parser._latestChangeReference = new BackgroundParser.ChangeReference(null, new StringTextSnapshot(string.Empty));
var args = new BackgroundParserResultsReadyEventArgs(
new BackgroundParser.ChangeReference(new SourceChange(0, 0, string.Empty), new StringTextSnapshot(string.Empty)),
TestRazorCodeDocument.CreateEmpty());
// Act
@ -138,12 +137,11 @@ namespace Microsoft.VisualStudio.Editor.Razor
parser.DocumentStructureChanged += (sender, e) => called = true;
var latestChange = new SourceChange(0, 0, string.Empty);
var latestSnapshot = documentTracker.TextBuffer.CurrentSnapshot;
parser._latestChangeReference = new DefaultVisualStudioRazorParser.ChangeReference(latestChange, latestSnapshot);
parser._latestChangeReference = new BackgroundParser.ChangeReference(latestChange, latestSnapshot);
var codeDocument = TestRazorCodeDocument.CreateEmpty();
codeDocument.SetSyntaxTree(RazorSyntaxTree.Parse(TestRazorSourceDocument.Create()));
var args = new DocumentStructureChangedEventArgs(
latestChange,
latestSnapshot,
var args = new BackgroundParserResultsReadyEventArgs(
parser._latestChangeReference,
codeDocument);
// Act
@ -154,6 +152,46 @@ namespace Microsoft.VisualStudio.Editor.Razor
}
}
[ForegroundFact]
public void OnDocumentStructureChanged_FiresForOnlyLatestTextBufferReparseEdit()
{
// Arrange
var documentTracker = CreateDocumentTracker();
using (var parser = new DefaultVisualStudioRazorParser(
Dispatcher,
documentTracker,
ProjectEngineFactory,
new DefaultErrorReporter(),
Mock.Of<VisualStudioCompletionBroker>()))
{
var called = false;
parser.DocumentStructureChanged += (sender, e) => called = true;
var latestSnapshot = documentTracker.TextBuffer.CurrentSnapshot;
parser._latestChangeReference = new BackgroundParser.ChangeReference(null, latestSnapshot);
var codeDocument = TestRazorCodeDocument.CreateEmpty();
codeDocument.SetSyntaxTree(RazorSyntaxTree.Parse(TestRazorSourceDocument.Create()));
var badArgs = new BackgroundParserResultsReadyEventArgs(
// This is a different reparse edit, shouldn't be fired for this call
new BackgroundParser.ChangeReference(null, latestSnapshot),
codeDocument);
var goodArgs = new BackgroundParserResultsReadyEventArgs(
parser._latestChangeReference,
codeDocument);
// Act - 1
parser.OnDocumentStructureChanged(badArgs);
// Assert - 1
Assert.False(called);
// Act - 2
parser.OnDocumentStructureChanged(goodArgs);
// Assert - 2
Assert.True(called);
}
}
[ForegroundFact]
public void StartIdleTimer_DoesNotRestartTimerWhenAlreadyRunning()
{