diff --git a/src/Microsoft.VisualStudio.Editor.Razor/BackgroundParser.cs b/src/Microsoft.VisualStudio.Editor.Razor/BackgroundParser.cs index 9a7cabb8e1..90f37f1273 100644 --- a/src/Microsoft.VisualStudio.Editor.Razor/BackgroundParser.cs +++ b/src/Microsoft.VisualStudio.Editor.Razor/BackgroundParser.cs @@ -30,7 +30,7 @@ namespace Microsoft.VisualStudio.Editor.Razor /// /// Fired on the main thread. /// - public event EventHandler ResultsReady; + public event EventHandler 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 _changes = new List(); + private IList _changes = new List(); public MainThreadState(string fileName) { @@ -124,7 +125,7 @@ namespace Microsoft.VisualStudio.Editor.Razor SetThreadId(Thread.CurrentThread.ManagedThreadId); } - public event EventHandler ResultsReady; + public event EventHandler 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(); + _changes = new List(); 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 _previouslyDiscarded = new List(); + private IList _previouslyDiscarded = new List(); 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 allEdits; + List 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 changes, CancellationToken cancelToken) + public WorkParcel(IList changes, CancellationToken cancelToken) { - Edits = changes; + Changes = changes; CancelToken = cancelToken; } public CancellationToken CancelToken { get; } - public IList Edits { get; } + public IList 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; } } } } diff --git a/src/Microsoft.VisualStudio.Editor.Razor/BackgroundParserResultsReadyEventArgs.cs b/src/Microsoft.VisualStudio.Editor.Razor/BackgroundParserResultsReadyEventArgs.cs new file mode 100644 index 0000000000..ecb9cef76e --- /dev/null +++ b/src/Microsoft.VisualStudio.Editor.Razor/BackgroundParserResultsReadyEventArgs.cs @@ -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; } + } +} diff --git a/src/Microsoft.VisualStudio.Editor.Razor/DefaultVisualStudioRazorParser.cs b/src/Microsoft.VisualStudio.Editor.Razor/DefaultVisualStudioRazorParser.cs index 0feda57bce..b34901659b 100644 --- a/src/Microsoft.VisualStudio.Editor.Razor/DefaultVisualStudioRazorParser.cs +++ b/src/Microsoft.VisualStudio.Editor.Razor/DefaultVisualStudioRazorParser.cs @@ -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; - } - } } } diff --git a/test/Microsoft.VisualStudio.Editor.Razor.Test/DefaultVisualStudioRazorParserTest.cs b/test/Microsoft.VisualStudio.Editor.Razor.Test/DefaultVisualStudioRazorParserTest.cs index d3ec3b9089..3df48c553f 100644 --- a/test/Microsoft.VisualStudio.Editor.Razor.Test/DefaultVisualStudioRazorParserTest.cs +++ b/test/Microsoft.VisualStudio.Editor.Razor.Test/DefaultVisualStudioRazorParserTest.cs @@ -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())) + { + 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() {