From a9a86fa3bf7afcbb34f64f798e6dc93d817e3537 Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Thu, 24 Aug 2017 07:58:18 -0700 Subject: [PATCH] Add foreground dispatcher --- .../ForegroundThreadAffinitizedObject.cs | 34 --------------- .../Editor/VisualStudioRazorParser.cs | 19 +++++---- .../ForegroundDispatcher.cs | 31 ++++++++++++++ .../Properties/Resources.Designer.cs | 42 +++++++++++++++++++ .../Resources.resx | 9 ++++ .../Editor/VisualStudioRazorParserTest.cs | 14 +++---- .../ForegroundDispatcherTestBase.cs | 19 +++++++++ 7 files changed, 120 insertions(+), 48 deletions(-) delete mode 100644 src/Microsoft.VisualStudio.LanguageServices.Razor/Editor/ForegroundThreadAffinitizedObject.cs create mode 100644 src/Microsoft.VisualStudio.LanguageServices.Razor/ForegroundDispatcher.cs create mode 100644 test/Microsoft.VisualStudio.LanguageServices.Razor.Test/ForegroundDispatcherTestBase.cs diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/Editor/ForegroundThreadAffinitizedObject.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/Editor/ForegroundThreadAffinitizedObject.cs deleted file mode 100644 index 720fb6841c..0000000000 --- a/src/Microsoft.VisualStudio.LanguageServices.Razor/Editor/ForegroundThreadAffinitizedObject.cs +++ /dev/null @@ -1,34 +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.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/VisualStudioRazorParser.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/Editor/VisualStudioRazorParser.cs index ce92f7f227..106efbca51 100644 --- a/src/Microsoft.VisualStudio.LanguageServices.Razor/Editor/VisualStudioRazorParser.cs +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/Editor/VisualStudioRazorParser.cs @@ -22,11 +22,16 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor private const int IdleDelay = 3000; private readonly ICompletionBroker _completionBroker; private readonly BackgroundParser _parser; - private readonly ForegroundThreadAffinitizedObject _foregroundThreadAffinitizedObject; + private readonly ForegroundDispatcher _dispatcher; private RazorSyntaxTreePartialParser _partialParser; - public VisualStudioRazorParser(ITextBuffer buffer, RazorTemplateEngine templateEngine, string filePath, ICompletionBroker completionBroker) + public VisualStudioRazorParser(ForegroundDispatcher dispatcher, ITextBuffer buffer, RazorTemplateEngine templateEngine, string filePath, ICompletionBroker completionBroker) { + if (dispatcher == null) + { + throw new ArgumentNullException(nameof(dispatcher)); + } + if (buffer == null) { throw new ArgumentNullException(nameof(buffer)); @@ -47,6 +52,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor throw new ArgumentNullException(nameof(completionBroker)); } + _dispatcher = dispatcher; TemplateEngine = templateEngine; FilePath = filePath; _textBuffer = buffer; @@ -56,7 +62,6 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor _idleTimer = new Timer(IdleDelay); _idleTimer.Elapsed += Onidle; _parser.ResultsReady += OnResultsReady; - _foregroundThreadAffinitizedObject = new ForegroundThreadAffinitizedObject(); _parser.Start(); } @@ -80,7 +85,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor public void Dispose() { - _foregroundThreadAffinitizedObject.AssertIsForeground(); + _dispatcher.AssertForegroundThread(); _textBuffer.Changed -= TextBuffer_OnChanged; _parser.Dispose(); @@ -89,7 +94,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor private void TextBuffer_OnChanged(object sender, TextContentChangedEventArgs contentChange) { - _foregroundThreadAffinitizedObject.AssertIsForeground(); + _dispatcher.AssertForegroundThread(); if (contentChange.Changes.Count > 0) { @@ -144,7 +149,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor private void Onidle(object sender, ElapsedEventArgs e) { - _foregroundThreadAffinitizedObject.AssertIsBackground(); + _dispatcher.AssertBackgroundThread(); var textViews = Array.Empty(); @@ -162,7 +167,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor private void OnResultsReady(object sender, DocumentStructureChangedEventArgs args) { - _foregroundThreadAffinitizedObject.AssertIsBackground(); + _dispatcher.AssertBackgroundThread(); if (DocumentStructureChanged != null) { diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/ForegroundDispatcher.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/ForegroundDispatcher.cs new file mode 100644 index 0000000000..712cf635a6 --- /dev/null +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/ForegroundDispatcher.cs @@ -0,0 +1,31 @@ +// 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.Runtime.CompilerServices; + +namespace Microsoft.VisualStudio.LanguageServices.Razor +{ + internal abstract class ForegroundDispatcher + { + public abstract bool IsForegroundThread { get; } + + public virtual void AssertForegroundThread([CallerMemberName] string caller = null) + { + if (!IsForegroundThread) + { + caller = caller == null ? Resources.ForegroundDispatcher_NoMethodNamePlaceholder : $"'{caller}'"; + throw new InvalidOperationException(Resources.FormatForegroundDispatcher_AssertForegroundThread(caller)); + } + } + + public virtual void AssertBackgroundThread([CallerMemberName] string caller = null) + { + if (IsForegroundThread) + { + caller = caller == null ? Resources.ForegroundDispatcher_NoMethodNamePlaceholder : $"'{caller}'"; + throw new InvalidOperationException(Resources.FormatForegroundDispatcher_AssertBackgroundThread(caller)); + } + } + } +} diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/Properties/Resources.Designer.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/Properties/Resources.Designer.cs index 2237686c54..f137c477f1 100644 --- a/src/Microsoft.VisualStudio.LanguageServices.Razor/Properties/Resources.Designer.cs +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/Properties/Resources.Designer.cs @@ -24,6 +24,48 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor internal static string FormatArgumentCannotBeNullOrEmpty() => GetString("ArgumentCannotBeNullOrEmpty"); + /// + /// {0} must be called on a background thread. + /// + internal static string ForegroundDispatcher_AssertBackgroundThread + { + get => GetString("ForegroundDispatcher_AssertBackgroundThread"); + } + + /// + /// {0} must be called on a background thread. + /// + internal static string FormatForegroundDispatcher_AssertBackgroundThread(object p0) + => string.Format(CultureInfo.CurrentCulture, GetString("ForegroundDispatcher_AssertBackgroundThread"), p0); + + /// + /// {0} must be called on the foreground thread. + /// + internal static string ForegroundDispatcher_AssertForegroundThread + { + get => GetString("ForegroundDispatcher_AssertForegroundThread"); + } + + /// + /// {0} must be called on the foreground thread. + /// + internal static string FormatForegroundDispatcher_AssertForegroundThread(object p0) + => string.Format(CultureInfo.CurrentCulture, GetString("ForegroundDispatcher_AssertForegroundThread"), p0); + + /// + /// The method + /// + internal static string ForegroundDispatcher_NoMethodNamePlaceholder + { + get => GetString("ForegroundDispatcher_NoMethodNamePlaceholder"); + } + + /// + /// The method + /// + internal static string FormatForegroundDispatcher_NoMethodNamePlaceholder() + => GetString("ForegroundDispatcher_NoMethodNamePlaceholder"); + /// /// An unexpected exception occurred when invoking '{0}.{1}' on the Razor language service. /// diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/Resources.resx b/src/Microsoft.VisualStudio.LanguageServices.Razor/Resources.resx index 87f559339f..3dfcb98b6f 100644 --- a/src/Microsoft.VisualStudio.LanguageServices.Razor/Resources.resx +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/Resources.resx @@ -120,6 +120,15 @@ Value cannot be null or an empty string. + + {0} must be called on a background thread. + + + {0} must be called on the foreground thread. + + + The method + An unexpected exception occurred when invoking '{0}.{1}' on the Razor language service. diff --git a/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Editor/VisualStudioRazorParserTest.cs b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Editor/VisualStudioRazorParserTest.cs index 81b0b3a7f6..81d97a3fde 100644 --- a/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Editor/VisualStudioRazorParserTest.cs +++ b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Editor/VisualStudioRazorParserTest.cs @@ -18,20 +18,20 @@ using Xunit; namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor { - public class VisualStudioRazorParserTest + public class VisualStudioRazorParserTest : ForegroundDispatcherTestBase { 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())); + Assert.Throws("filePath", () => new VisualStudioRazorParser(Dispatcher, new TestTextBuffer(null), CreateTemplateEngine(), null, new TestCompletionBroker())); } [Fact] public void ConstructorRequiresNonEmptyPhysicalPath() { - Assert.Throws("filePath", () => new VisualStudioRazorParser(new TestTextBuffer(null), CreateTemplateEngine(), string.Empty, new TestCompletionBroker())); + Assert.Throws("filePath", () => new VisualStudioRazorParser(Dispatcher, new TestTextBuffer(null), CreateTemplateEngine(), string.Empty, new TestCompletionBroker())); } [Fact] @@ -40,7 +40,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor // Arrange var original = new StringTextSnapshot("Foo @bar Baz"); var testBuffer = new TestTextBuffer(original); - using (var parser = new VisualStudioRazorParser(testBuffer, CreateTemplateEngine(), TestLinePragmaFileName, new TestCompletionBroker())) + using (var parser = new VisualStudioRazorParser(Dispatcher, testBuffer, CreateTemplateEngine(), TestLinePragmaFileName, new TestCompletionBroker())) { parser._idleTimer.Interval = 100; var changed = new StringTextSnapshot("Foo @bap Daz"); @@ -519,9 +519,9 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor RunTypeKeywordTest("class"); } - private static TestParserManager CreateParserManager(ITextSnapshot originalSnapshot) + private TestParserManager CreateParserManager(ITextSnapshot originalSnapshot, int idleDelay = 50) { - var parser = new VisualStudioRazorParser(new TestTextBuffer(originalSnapshot), CreateTemplateEngine(), TestLinePragmaFileName, new TestCompletionBroker()); + var parser = new VisualStudioRazorParser(Dispatcher, new TestTextBuffer(originalSnapshot), CreateTemplateEngine(), TestLinePragmaFileName, new TestCompletionBroker()); return new TestParserManager(parser); } @@ -552,7 +552,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor return templateEngine; } - private static void RunTypeKeywordTest(string keyword) + private void RunTypeKeywordTest(string keyword) { // Arrange var before = "@" + keyword.Substring(0, keyword.Length - 1); diff --git a/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/ForegroundDispatcherTestBase.cs b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/ForegroundDispatcherTestBase.cs new file mode 100644 index 0000000000..dbe8bbdae0 --- /dev/null +++ b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/ForegroundDispatcherTestBase.cs @@ -0,0 +1,19 @@ +// 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.Threading; + +namespace Microsoft.VisualStudio.LanguageServices.Razor +{ + public abstract class ForegroundDispatcherTestBase + { + internal ForegroundDispatcher Dispatcher { get; } = new SingleThreadedForegroundDispatcher(); + + private class SingleThreadedForegroundDispatcher : ForegroundDispatcher + { + private Thread Thread { get; } = Thread.CurrentThread; + + public override bool IsForegroundThread => Thread.CurrentThread == Thread; + } + } +}