diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/DefaultErrorReporter.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/DefaultErrorReporter.cs new file mode 100644 index 0000000000..664434a674 --- /dev/null +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/DefaultErrorReporter.cs @@ -0,0 +1,20 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +namespace Microsoft.CodeAnalysis.Razor +{ + internal class DefaultErrorReporter : ErrorReporter + { + public override void ReportError(Exception exception) + { + // Do nothing. + } + + public override void ReportError(Exception exception, Project project) + { + // Do nothing. + } + } +} diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/DefaultErrorReporterFactory.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/DefaultErrorReporterFactory.cs new file mode 100644 index 0000000000..102ef6551b --- /dev/null +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/DefaultErrorReporterFactory.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.Composition; +using Microsoft.CodeAnalysis.Host; +using Microsoft.CodeAnalysis.Host.Mef; + +namespace Microsoft.CodeAnalysis.Razor +{ + [Shared] + [ExportWorkspaceServiceFactory(typeof(ErrorReporter))] + internal class DefaultErrorReporterFactory : IWorkspaceServiceFactory + { + public IWorkspaceService CreateService(HostWorkspaceServices workspaceServices) + { + return new DefaultErrorReporter(); + } + } +} diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ErrorReporter.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ErrorReporter.cs new file mode 100644 index 0000000000..03bc44f61c --- /dev/null +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ErrorReporter.cs @@ -0,0 +1,15 @@ +// 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.CodeAnalysis.Host; + +namespace Microsoft.CodeAnalysis.Razor +{ + internal abstract class ErrorReporter : IWorkspaceService + { + public abstract void ReportError(Exception exception); + + public abstract void ReportError(Exception exception, Project project); + } +} diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultProjectSnapshotListener.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultProjectSnapshotListener.cs deleted file mode 100644 index 79ef8206d2..0000000000 --- a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultProjectSnapshotListener.cs +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; - -namespace Microsoft.CodeAnalysis.Razor.ProjectSystem -{ - internal class DefaultProjectSnapshotListener : ProjectSnapshotListener - { - public override event EventHandler ProjectChanged; - - internal void Notify(ProjectChangeEventArgs e) - { - var handler = ProjectChanged; - if (handler != null) - { - handler(this, e); - } - } - } -} diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultProjectSnapshotManager.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultProjectSnapshotManager.cs index 78820f1d9a..e8f9ff1de3 100644 --- a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultProjectSnapshotManager.cs +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultProjectSnapshotManager.cs @@ -9,12 +9,38 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem { internal class DefaultProjectSnapshotManager : ProjectSnapshotManagerBase { - private readonly ProjectSnapshotChangeTrigger[] _triggers; - private readonly Dictionary _projects; - private readonly List> _listeners; + public override event EventHandler Changed; - public DefaultProjectSnapshotManager(IEnumerable triggers, Workspace workspace) + private readonly ErrorReporter _errorReporter; + private readonly ForegroundDispatcher _foregroundDispatcher; + private readonly ProjectSnapshotChangeTrigger[] _triggers; + private readonly ProjectSnapshotWorkerQueue _workerQueue; + private readonly ProjectSnapshotWorker _worker; + + private readonly Dictionary _projects; + + public DefaultProjectSnapshotManager( + ForegroundDispatcher foregroundDispatcher, + ErrorReporter errorReporter, + ProjectSnapshotWorker worker, + IEnumerable triggers, + Workspace workspace) { + if (foregroundDispatcher == null) + { + throw new ArgumentNullException(nameof(foregroundDispatcher)); + } + + if (errorReporter == null) + { + throw new ArgumentNullException(nameof(errorReporter)); + } + + if (worker == null) + { + throw new ArgumentNullException(nameof(worker)); + } + if (triggers == null) { throw new ArgumentNullException(nameof(triggers)); @@ -25,11 +51,14 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem throw new ArgumentNullException(nameof(workspace)); } + _foregroundDispatcher = foregroundDispatcher; + _errorReporter = errorReporter; + _worker = worker; _triggers = triggers.ToArray(); Workspace = workspace; _projects = new Dictionary(); - _listeners = new List>(); + _workerQueue = new ProjectSnapshotWorkerQueue(_foregroundDispatcher, this, worker); for (var i = 0; i < _triggers.Length; i++) { @@ -37,18 +66,27 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem } } - public override IReadOnlyList Projects => _projects.Values.ToArray(); + public override IReadOnlyList Projects + { + get + { + return _projects.Values.ToArray(); + } + } + + public DefaultProjectSnapshot FindProject(ProjectId id) + { + if (id == null) + { + throw new ArgumentNullException(nameof(id)); + } + + _projects.TryGetValue(id, out var project); + return project; + } public override Workspace Workspace { get; } - public override ProjectSnapshotListener Subscribe() - { - var subscription = new DefaultProjectSnapshotListener(); - _listeners.Add(new WeakReference(subscription)); - - return subscription; - } - public override void ProjectAdded(Project underlyingProject) { if (underlyingProject == null) @@ -60,7 +98,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem _projects[underlyingProject.Id] = snapshot; // New projects always start dirty, need to compute state in the background. - NotifyBackgroundWorker(); + NotifyBackgroundWorker(snapshot.UnderlyingProject); // We need to notify listeners about every project add. NotifyListeners(new ProjectChangeEventArgs(snapshot, ProjectChangeKind.Added)); @@ -84,12 +122,12 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem { // We don't need to notify listeners yet because we don't have any **new** computed state. However we do // need to trigger the background work to asynchronously compute the effect of the updates. - NotifyBackgroundWorker(); + NotifyBackgroundWorker(snapshot.UnderlyingProject); } } } - public override void ProjectChanged(ProjectSnapshotUpdateContext update) + public override void ProjectUpdated(ProjectSnapshotUpdateContext update) { if (update == null) { @@ -106,7 +144,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem { // It's possible that the snapshot can still be dirty if we got a project update while computing state in // the background. We need to trigger the background work to asynchronously compute the effect of the updates. - NotifyBackgroundWorker(); + NotifyBackgroundWorker(snapshot.UnderlyingProject); } // Now we need to know if the changes that we applied are significant. If that's the case then @@ -146,25 +184,29 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem } // virtual so it can be overridden in tests - protected virtual void NotifyBackgroundWorker() + protected virtual void NotifyBackgroundWorker(Project project) { - + _workerQueue.Enqueue(project); } // virtual so it can be overridden in tests protected virtual void NotifyListeners(ProjectChangeEventArgs e) { - for (var i = 0; i < _listeners.Count; i++) + var handler = Changed; + if (handler != null) { - if (_listeners[i].TryGetTarget(out var listener)) - { - listener.Notify(e); - } - else - { - _listeners.RemoveAt(i--); - } + handler(this, e); } } + + public override void ReportError(Exception exception) + { + _errorReporter.ReportError(exception); + } + + public override void ReportError(Exception exception, Project project) + { + _errorReporter.ReportError(exception, project); + } } } diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultProjectSnapshotManagerFactory.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultProjectSnapshotManagerFactory.cs index f51ed55ab9..82d0bcdb53 100644 --- a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultProjectSnapshotManagerFactory.cs +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultProjectSnapshotManagerFactory.cs @@ -16,7 +16,8 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem private readonly IEnumerable _triggers; [ImportingConstructor] - public DefaultProjectSnapshotManagerFactory([ImportMany] IEnumerable triggers) + public DefaultProjectSnapshotManagerFactory( + [ImportMany] IEnumerable triggers) { _triggers = triggers; } @@ -28,7 +29,12 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem throw new ArgumentNullException(nameof(languageServices)); } - return new DefaultProjectSnapshotManager(_triggers, languageServices.WorkspaceServices.Workspace); + return new DefaultProjectSnapshotManager( + languageServices.WorkspaceServices.GetRequiredService(), + languageServices.WorkspaceServices.GetRequiredService(), + languageServices.GetRequiredService(), + _triggers, + languageServices.WorkspaceServices.Workspace); } } } diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultProjectSnapshotWorker.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultProjectSnapshotWorker.cs new file mode 100644 index 0000000000..78efdfbf04 --- /dev/null +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultProjectSnapshotWorker.cs @@ -0,0 +1,59 @@ +// 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; +using System.Threading.Tasks; + +namespace Microsoft.CodeAnalysis.Razor.ProjectSystem +{ + internal class DefaultProjectSnapshotWorker : ProjectSnapshotWorker + { + private readonly ProjectExtensibilityConfigurationFactory _configurationFactory; + private readonly ForegroundDispatcher _foregroundDispatcher; + + public DefaultProjectSnapshotWorker( + ForegroundDispatcher foregroundDispatcher, + ProjectExtensibilityConfigurationFactory configurationFactory) + { + if (foregroundDispatcher == null) + { + throw new ArgumentNullException(nameof(foregroundDispatcher)); + } + + if (configurationFactory == null) + { + throw new ArgumentNullException(nameof(configurationFactory)); + } + + _foregroundDispatcher = foregroundDispatcher; + _configurationFactory = configurationFactory; + } + + public override Task ProcessUpdateAsync(ProjectSnapshotUpdateContext update, CancellationToken cancellationToken = default(CancellationToken)) + { + if (update == null) + { + throw new ArgumentNullException(nameof(update)); + } + + // Don't block the main thread + if (_foregroundDispatcher.IsForegroundThread) + { + return Task.Factory.StartNew(ProjectUpdatesCoreAsync, update, CancellationToken.None, TaskCreationOptions.None, _foregroundDispatcher.BackgroundScheduler); + } + + return ProjectUpdatesCoreAsync(update); + } + + private async Task ProjectUpdatesCoreAsync(object state) + { + var update = (ProjectSnapshotUpdateContext)state; + + // We'll have more things to process here, but for now we're just hardcoding the configuration. + + var configuration = await _configurationFactory.GetConfigurationAsync(update.UnderlyingProject); + update.Configuration = configuration; + } + } +} diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultProjectSnapshotWorkerFactory.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultProjectSnapshotWorkerFactory.cs new file mode 100644 index 0000000000..40a5b3f5f4 --- /dev/null +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultProjectSnapshotWorkerFactory.cs @@ -0,0 +1,21 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Composition; +using Microsoft.CodeAnalysis.Host; +using Microsoft.CodeAnalysis.Host.Mef; + +namespace Microsoft.CodeAnalysis.Razor.ProjectSystem +{ + [Shared] + [ExportLanguageServiceFactory(typeof(ProjectSnapshotWorker), RazorLanguage.Name)] + internal class DefaultProjectSnapshotWorkerFactory : ILanguageServiceFactory + { + public ILanguageService CreateLanguageService(HostLanguageServices languageServices) + { + return new DefaultProjectSnapshotWorker( + languageServices.WorkspaceServices.GetRequiredService(), + languageServices.GetRequiredService()); + } + } +} diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/MvcExtensibilityConfiguration.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/MvcExtensibilityConfiguration.cs index 9cf1d19973..8f63970733 100644 --- a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/MvcExtensibilityConfiguration.cs +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/MvcExtensibilityConfiguration.cs @@ -34,6 +34,9 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem public override IReadOnlyList Assemblies { get; } + // MVC: '2.0.0' (fallback) or MVC: '2.1.3' + public override string DisplayName => $"MVC: {MvcAssembly.Identity.Version.ToString(3)}" + (Kind == ProjectExtensibilityConfigurationKind.Fallback? " (fallback)" : string.Empty); + public override ProjectExtensibilityConfigurationKind Kind { get; } public override ProjectExtensibilityAssembly RazorAssembly { get; } @@ -48,7 +51,10 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem } // We're intentionally ignoring the 'Kind' here. That's mostly for diagnostics and doesn't influence any behavior. - return Enumerable.SequenceEqual(Assemblies.OrderBy(a => a.Identity.Name), other.Assemblies.OrderBy(a => a.Identity.Name)); + return Enumerable.SequenceEqual( + Assemblies.OrderBy(a => a.Identity.Name).Select(a => a.Identity), + other.Assemblies.OrderBy(a => a.Identity.Name).Select(a => a.Identity), + AssemblyIdentityEqualityComparer.NameAndVersion); } public override int GetHashCode() @@ -61,5 +67,10 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem return hash; } + + public override string ToString() + { + return DisplayName; + } } } diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectExtensibilityConfiguration.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectExtensibilityConfiguration.cs index f230160380..23115a334a 100644 --- a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectExtensibilityConfiguration.cs +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectExtensibilityConfiguration.cs @@ -10,6 +10,8 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem { public abstract IReadOnlyList Assemblies { get; } + public abstract string DisplayName { get; } + public abstract ProjectExtensibilityConfigurationKind Kind { get; } public abstract ProjectExtensibilityAssembly RazorAssembly { get; } @@ -20,7 +22,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem public override bool Equals(object obj) { - return base.Equals(obj as ProjectExtensibilityConfiguration); + return Equals(obj as ProjectExtensibilityConfiguration); } } } diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSnapshotListener.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSnapshotListener.cs deleted file mode 100644 index 08e3c295d9..0000000000 --- a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSnapshotListener.cs +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; - -namespace Microsoft.CodeAnalysis.Razor.ProjectSystem -{ - internal abstract class ProjectSnapshotListener - { - public abstract event EventHandler ProjectChanged; - } -} diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSnapshotManager.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSnapshotManager.cs index c19a8da95b..d5a4c7a9ea 100644 --- a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSnapshotManager.cs +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSnapshotManager.cs @@ -1,6 +1,7 @@ // 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.CodeAnalysis.Host; @@ -8,8 +9,9 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem { internal abstract class ProjectSnapshotManager : ILanguageService { + public abstract event EventHandler Changed; + public abstract IReadOnlyList Projects { get; } - public abstract ProjectSnapshotListener Subscribe(); } } diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSnapshotManagerBase.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSnapshotManagerBase.cs index c738ce9182..1e036b76f6 100644 --- a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSnapshotManagerBase.cs +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSnapshotManagerBase.cs @@ -1,6 +1,8 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System; + namespace Microsoft.CodeAnalysis.Razor.ProjectSystem { internal abstract class ProjectSnapshotManagerBase : ProjectSnapshotManager @@ -11,10 +13,14 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem public abstract void ProjectChanged(Project underlyingProject); - public abstract void ProjectChanged(ProjectSnapshotUpdateContext update); + public abstract void ProjectUpdated(ProjectSnapshotUpdateContext update); public abstract void ProjectRemoved(Project underlyingProject); public abstract void ProjectsCleared(); + + public abstract void ReportError(Exception exception); + + public abstract void ReportError(Exception exception, Project project); } } diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSnapshotWorker.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSnapshotWorker.cs new file mode 100644 index 0000000000..5c6288ee22 --- /dev/null +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSnapshotWorker.cs @@ -0,0 +1,14 @@ +// 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; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Host; + +namespace Microsoft.CodeAnalysis.Razor.ProjectSystem +{ + internal abstract class ProjectSnapshotWorker : ILanguageService + { + public abstract Task ProcessUpdateAsync(ProjectSnapshotUpdateContext update, CancellationToken cancellationToken = default(CancellationToken)); + } +} diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSnapshotWorkerQueue.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSnapshotWorkerQueue.cs new file mode 100644 index 0000000000..4ee7da84d4 --- /dev/null +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSnapshotWorkerQueue.cs @@ -0,0 +1,210 @@ +// 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.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.CodeAnalysis.Razor.ProjectSystem +{ + internal class ProjectSnapshotWorkerQueue + { + private readonly ForegroundDispatcher _foregroundDispatcher; + private readonly DefaultProjectSnapshotManager _projectManager; + private readonly ProjectSnapshotWorker _projectWorker; + + private readonly Dictionary _projects; + private Timer _timer; + + public ProjectSnapshotWorkerQueue(ForegroundDispatcher foregroundDispatcher, DefaultProjectSnapshotManager projectManager, ProjectSnapshotWorker projectWorker) + { + if (foregroundDispatcher == null) + { + throw new ArgumentNullException(nameof(foregroundDispatcher)); + } + + if (projectManager == null) + { + throw new ArgumentNullException(nameof(projectManager)); + } + + if (projectWorker == null) + { + throw new ArgumentNullException(nameof(projectWorker)); + } + + _foregroundDispatcher = foregroundDispatcher; + _projectManager = projectManager; + _projectWorker = projectWorker; + + _projects = new Dictionary(); + } + + public bool HasPendingNotifications + { + get + { + lock (_projects) + { + return _projects.Count > 0; + } + } + } + + // Used in unit tests to control the timer delay. + public TimeSpan Delay { get; set; } = TimeSpan.FromSeconds(2); + +#if DEBUG + public bool IsScheduledOrRunning => _timer != null; + + // Used in unit tests to ensure we can control when background work starts. + public ManualResetEventSlim BlockBackgroundWorkStart { get; set; } + + // Used in unit tests to ensure we can know when background work finishes. + public ManualResetEventSlim NotifyBackgroundWorkFinish { get; set; } + + // Used in unit tests to ensure we can be notified when all completes. + public ManualResetEventSlim NotifyForegroundWorkFinish { get; set; } +#endif + + [Conditional("DEBUG")] + private void OnStartingBackgroundWork() + { + + if (BlockBackgroundWorkStart != null) + { + BlockBackgroundWorkStart.Wait(); + BlockBackgroundWorkStart.Reset(); + } + } + + [Conditional("DEBUG")] + private void OnFinishingBackgroundWork() + { + if (NotifyBackgroundWorkFinish != null) + { + NotifyBackgroundWorkFinish.Set(); + } + } + + [Conditional("DEBUG")] + private void OnFinishingForegroundWork() + { + if (NotifyForegroundWorkFinish != null) + { + NotifyForegroundWorkFinish.Set(); + } + } + + public void Enqueue(Project project) + { + if (project == null) + { + throw new ArgumentNullException(); + } + + _foregroundDispatcher.AssertForegroundThread(); + + lock (_projects) + { + // We only want to store the last 'seen' version of any given project. That way when we pick one to process + // it's always the best version to use. + _projects[project.Id] = project; + + StartWorker(); + } + } + + protected virtual void StartWorker() + { + // Access to the timer is protected by the lock in Enqueue and in Timer_Tick + if (_timer == null) + { + // Timer will fire after a fixed delay, but only once. + _timer = new Timer(Timer_Tick, null, Delay, Timeout.InfiniteTimeSpan); + } + } + + private async void Timer_Tick(object state) // Yeah I know. + { + try + { + _foregroundDispatcher.AssertBackgroundThread(); + + // Timer is stopped. + _timer.Change(Timeout.Infinite, Timeout.Infinite); + + OnStartingBackgroundWork(); + + Project[] work; + lock (_projects) + { + work = _projects.Values.ToArray(); + _projects.Clear(); + } + + var updates = new(ProjectSnapshotUpdateContext context, Exception exception)[work.Length]; + for (var i = 0; i < work.Length; i++) + { + try + { + updates[i] = (new ProjectSnapshotUpdateContext(work[i]), null); + await _projectWorker.ProcessUpdateAsync(updates[i].context); + } + catch (Exception projectException) + { + updates[i] = (updates[i].context, projectException); + } + } + + OnFinishingBackgroundWork(); + + // We need to get back to the UI thread to update the project system. + await Task.Factory.StartNew(PersistUpdates, updates, CancellationToken.None, TaskCreationOptions.None, _foregroundDispatcher.ForegroundScheduler); + + lock (_projects) + { + // Resetting the timer allows another batch of work to start. + _timer.Dispose(); + _timer = null; + + // If more work came in while we were running start the worker again. + if (_projects.Count > 0) + { + StartWorker(); + } + } + + OnFinishingForegroundWork(); + } + catch (Exception ex) + { + // This is something totally unexpected, let's just send it over to the workspace. + await Task.Factory.StartNew(() => _projectManager.ReportError(ex), CancellationToken.None, TaskCreationOptions.None, _foregroundDispatcher.ForegroundScheduler); + } + } + + private void PersistUpdates(object state) + { + _foregroundDispatcher.AssertForegroundThread(); + + var updates = ((ProjectSnapshotUpdateContext context, Exception exception)[])state; + + for (var i = 0; i < updates.Length; i++) + { + var update = updates[i]; + if (update.exception == null) + { + _projectManager.ProjectUpdated(update.context); + } + else + { + _projectManager.ReportError(update.exception, update.context?.UnderlyingProject); + } + } + } + } +} diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/WorkspaceProjectSnapshotChangeTrigger.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/WorkspaceProjectSnapshotChangeTrigger.cs index 2781949bf0..1769da0197 100644 --- a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/WorkspaceProjectSnapshotChangeTrigger.cs +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/WorkspaceProjectSnapshotChangeTrigger.cs @@ -27,7 +27,10 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem foreach (var project in solution.Projects) { - _projectManager.ProjectAdded(project); + if (project.Language == LanguageNames.CSharp) + { + _projectManager.ProjectAdded(project); + } } } @@ -42,7 +45,10 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem underlyingProject = e.NewSolution.GetProject(e.ProjectId); Debug.Assert(underlyingProject != null); - _projectManager.ProjectAdded(underlyingProject); + if (underlyingProject.Language == LanguageNames.CSharp) + { + _projectManager.ProjectAdded(underlyingProject); + } break; } diff --git a/src/Microsoft.CodeAnalysis.Razor/AssemblyIdentityEqualityComparer.cs b/src/Microsoft.CodeAnalysis.Razor/AssemblyIdentityEqualityComparer.cs new file mode 100644 index 0000000000..e41fc02abc --- /dev/null +++ b/src/Microsoft.CodeAnalysis.Razor/AssemblyIdentityEqualityComparer.cs @@ -0,0 +1,50 @@ +// 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.Extensions.Internal; + +namespace Microsoft.CodeAnalysis.Razor +{ + internal abstract class AssemblyIdentityEqualityComparer : IEqualityComparer + { + public static readonly AssemblyIdentityEqualityComparer NameAndVersion = new NameAndVersionEqualityComparer(); + + public abstract bool Equals(AssemblyIdentity x, AssemblyIdentity y); + + public abstract int GetHashCode(AssemblyIdentity obj); + + private class NameAndVersionEqualityComparer : AssemblyIdentityEqualityComparer + { + public override bool Equals(AssemblyIdentity x, AssemblyIdentity y) + { + if (object.ReferenceEquals(x, y)) + { + return true; + } + else if (x == null ^ y == null) + { + return false; + } + else + { + return string.Equals(x.Name, y.Name, StringComparison.OrdinalIgnoreCase) && object.Equals(x.Version, y.Version); + } + } + + public override int GetHashCode(AssemblyIdentity obj) + { + if (obj == null) + { + return 0; + } + + var hash = new HashCodeCombiner(); + hash.Add(obj.Name, StringComparer.OrdinalIgnoreCase); + hash.Add(obj.Version); + return hash; + } + } + } +} diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/DefaultTagHelperResolver.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/DefaultTagHelperResolver.cs index ba9e359f46..5ed80730d7 100644 --- a/src/Microsoft.VisualStudio.LanguageServices.Razor/DefaultTagHelperResolver.cs +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/DefaultTagHelperResolver.cs @@ -19,17 +19,22 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor { internal class DefaultTagHelperResolver : TagHelperResolver { + private readonly ErrorReporter _errorReporter; private readonly Workspace _workspace; - private readonly IServiceProvider _services; - public DefaultTagHelperResolver(Workspace workspace, IServiceProvider services) + public DefaultTagHelperResolver(ErrorReporter errorReporter, Workspace workspace) { + _errorReporter = errorReporter; _workspace = workspace; - _services = services; } public async Task GetTagHelpersAsync(Project project) { + if (project == null) + { + throw new ArgumentNullException(nameof(project)); + } + try { TagHelperResolutionResult result; @@ -67,15 +72,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor } catch (Exception exception) { - var log = GetActivityLog(); - if (log != null) - { - var hr = log.LogEntry( - (uint)__ACTIVITYLOG_ENTRYTYPE.ALE_ERROR, - "Razor Language Services", - $"Error discovering TagHelpers:{Environment.NewLine}{exception}"); - ErrorHandler.ThrowOnFailure(hr); - } + _errorReporter.ReportError(exception, project); throw new RazorLanguageServiceException( typeof(DefaultTagHelperResolver).FullName, @@ -121,10 +118,5 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor return serializer.Deserialize(reader); } } - - private IVsActivityLog GetActivityLog() - { - return _services.GetService(typeof(SVsActivityLog)) as IVsActivityLog; - } } } diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/DefaultTagHelperResolverFactory.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/DefaultTagHelperResolverFactory.cs index 126148e4f4..f59d049bfa 100644 --- a/src/Microsoft.VisualStudio.LanguageServices.Razor/DefaultTagHelperResolverFactory.cs +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/DefaultTagHelperResolverFactory.cs @@ -5,7 +5,6 @@ using System.ComponentModel.Composition; using Microsoft.CodeAnalysis.Host; using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.Razor; -using Microsoft.VisualStudio.Shell; namespace Microsoft.VisualStudio.LanguageServices.Razor { @@ -15,12 +14,9 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor [Import] public VisualStudioWorkspace Workspace { get; set; } - [Import] - public SVsServiceProvider Services { get; set; } - public ILanguageService CreateLanguageService(HostLanguageServices languageServices) { - return new DefaultTagHelperResolver(Workspace, Services); + return new DefaultTagHelperResolver(Workspace.Services.GetRequiredService(), Workspace); } } } \ No newline at end of file diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/Editor/DefaultVisualStudioDocumentTracker.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/Editor/DefaultVisualStudioDocumentTracker.cs index 900e35f1bd..3bdd04a3e4 100644 --- a/src/Microsoft.VisualStudio.LanguageServices.Razor/Editor/DefaultVisualStudioDocumentTracker.cs +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/Editor/DefaultVisualStudioDocumentTracker.cs @@ -23,7 +23,6 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor private bool _isSupportedProject; private ProjectSnapshot _project; private string _projectPath; - private ProjectSnapshotListener _subscription; public override event EventHandler ContextChanged; @@ -59,13 +58,13 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor _workspace = workspace; // For now we assume that the workspace is the always default VS workspace. _textViews = new List(); - - Initialize(); } + internal override ProjectExtensibilityConfiguration Configuration => _project.Configuration; + public override bool IsSupportedProject => _isSupportedProject; - public override Project Project => _project?.UnderlyingProject; + public override Project Project => _workspace.CurrentSolution.GetProject(_project.UnderlyingProject.Id); public override ITextBuffer TextBuffer => _textBuffer; @@ -75,7 +74,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor public override Workspace Workspace => _workspace; - private void Initialize() + public void Subscribe() { // Fundamentally we have a Razor half of the world as as soon as the document is open - and then later // the C# half of the world will be initialized. This code is in general pretty tolerant of @@ -101,17 +100,19 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor return; } - var project = _projectManager.GetProjectWithFilePath(projectPath); - - var subscription = _projectManager.Subscribe(); - subscription.ProjectChanged += Subscription_ProjectStateChanged; - _isSupportedProject = isSupportedProject; _projectPath = projectPath; - _project = project; - _subscription = subscription; + _project = _projectManager.GetProjectWithFilePath(projectPath); + _projectManager.Changed += ProjectManager_Changed; + + OnContextChanged(_project); } - + + public void Unsubscribe() + { + _projectManager.Changed -= ProjectManager_Changed; + } + private void OnContextChanged(ProjectSnapshot project) { _project = project; @@ -123,7 +124,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor } } - private void Subscription_ProjectStateChanged(object sender, ProjectChangeEventArgs e) + private void ProjectManager_Changed(object sender, ProjectChangeEventArgs e) { if (_projectPath != null && string.Equals(_projectPath, e.Project.UnderlyingProject.FilePath, StringComparison.OrdinalIgnoreCase)) diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/Editor/DefaultVisualStudioDocumentTrackerFactory.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/Editor/DefaultVisualStudioDocumentTrackerFactory.cs index 24bfd0667c..3860e64882 100644 --- a/src/Microsoft.VisualStudio.LanguageServices.Razor/Editor/DefaultVisualStudioDocumentTrackerFactory.cs +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/Editor/DefaultVisualStudioDocumentTrackerFactory.cs @@ -28,15 +28,9 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor [ImportingConstructor] public DefaultVisualStudioDocumentTrackerFactory( - ForegroundDispatcher foregroundDispatcher, TextBufferProjectService projectService, [Import(typeof(VisualStudioWorkspace))] Workspace workspace) { - if (foregroundDispatcher == null) - { - throw new ArgumentNullException(nameof(foregroundDispatcher)); - } - if (projectService == null) { throw new ArgumentNullException(nameof(projectService)); @@ -46,11 +40,11 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor { throw new ArgumentNullException(nameof(workspace)); } - - _foregroundDispatcher = foregroundDispatcher; + _projectService = projectService; _workspace = workspace; + _foregroundDispatcher = workspace.Services.GetRequiredService(); _projectManager = workspace.Services.GetLanguageServices(RazorLanguage.Name).GetRequiredService(); } @@ -151,6 +145,10 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor if (!tracker.TextViewsInternal.Contains(textView)) { tracker.TextViewsInternal.Add(textView); + if (tracker.TextViewsInternal.Count == 1) + { + tracker.Subscribe(); + } } } } @@ -182,6 +180,10 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor if (textBuffer.Properties.TryGetProperty(typeof(VisualStudioDocumentTracker), out tracker)) { tracker.TextViewsInternal.Remove(textView); + if (tracker.TextViewsInternal.Count == 0) + { + tracker.Unsubscribe(); + } } } } diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/Editor/VisualStudioDocumentTracker.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/Editor/VisualStudioDocumentTracker.cs index 50551997fd..745ebbe837 100644 --- a/src/Microsoft.VisualStudio.LanguageServices.Razor/Editor/VisualStudioDocumentTracker.cs +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/Editor/VisualStudioDocumentTracker.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Razor.ProjectSystem; using Microsoft.VisualStudio.Text; using Microsoft.VisualStudio.Text.Editor; @@ -13,6 +14,8 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor { public abstract event EventHandler ContextChanged; + internal abstract ProjectExtensibilityConfiguration Configuration { get; } + public abstract bool IsSupportedProject { get; } public abstract Project Project { get; } diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/Legacy/LegacyTagHelperResolver.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/Legacy/LegacyTagHelperResolver.cs index 7a073d48af..a646026ba1 100644 --- a/src/Microsoft.VisualStudio.LanguageServices.Razor/Legacy/LegacyTagHelperResolver.cs +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/Legacy/LegacyTagHelperResolver.cs @@ -1,10 +1,9 @@ // 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.ComponentModel.Composition; using Microsoft.CodeAnalysis; -using Microsoft.VisualStudio.Shell; +using Microsoft.CodeAnalysis.Razor; namespace Microsoft.VisualStudio.LanguageServices.Razor { @@ -17,9 +16,8 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor { [ImportingConstructor] public LegacyTagHelperResolver( - [Import(typeof(VisualStudioWorkspace))] Workspace workspace, - [Import(typeof(SVsServiceProvider))] IServiceProvider services) : - base(workspace, services) + [Import(typeof(VisualStudioWorkspace))] Workspace workspace) + : base(workspace.Services.GetRequiredService(), workspace) { } } diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/VisualStudioErrorReporter.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/VisualStudioErrorReporter.cs new file mode 100644 index 0000000000..8a847f5a3a --- /dev/null +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/VisualStudioErrorReporter.cs @@ -0,0 +1,66 @@ +// 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.CodeAnalysis; +using Microsoft.CodeAnalysis.Razor; +using Microsoft.VisualStudio.Shell.Interop; + +namespace Microsoft.VisualStudio.LanguageServices.Razor +{ + internal class VisualStudioErrorReporter : ErrorReporter + { + private readonly IServiceProvider _services; + + public VisualStudioErrorReporter(IServiceProvider services) + { + if (services == null) + { + throw new ArgumentNullException(nameof(services)); + } + + _services = services; + } + + public override void ReportError(Exception exception) + { + if (exception == null) + { + return; + } + + var activityLog = GetActivityLog(); + if (activityLog != null) + { + var hr = activityLog.LogEntry( + (uint)__ACTIVITYLOG_ENTRYTYPE.ALE_ERROR, + "Razor Language Services", + $"Error encountered:{Environment.NewLine}{exception}"); + ErrorHandler.ThrowOnFailure(hr); + } + } + + public override void ReportError(Exception exception, Project project) + { + if (exception == null) + { + return; + } + + var activityLog = GetActivityLog(); + if (activityLog != null) + { + var hr = activityLog.LogEntry( + (uint)__ACTIVITYLOG_ENTRYTYPE.ALE_ERROR, + "Razor Language Services", + $"Error encountered from project '{project?.Name}':{Environment.NewLine}{exception}"); + ErrorHandler.ThrowOnFailure(hr); + } + } + + private IVsActivityLog GetActivityLog() + { + return _services.GetService(typeof(SVsActivityLog)) as IVsActivityLog; + } + } +} diff --git a/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/ProjectSystem/DefaultProjectSnapshotManagerTest.cs b/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/ProjectSystem/DefaultProjectSnapshotManagerTest.cs index 61a69c92a2..53e4b476c0 100644 --- a/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/ProjectSystem/DefaultProjectSnapshotManagerTest.cs +++ b/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/ProjectSystem/DefaultProjectSnapshotManagerTest.cs @@ -47,7 +47,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem // Adding some computed state var configuration = Mock.Of(); - ProjectManager.ProjectChanged(new ProjectSnapshotUpdateContext(project) { Configuration = configuration }); + ProjectManager.ProjectUpdated(new ProjectSnapshotUpdateContext(project) { Configuration = configuration }); ProjectManager.Reset(); project = project.WithAssemblyName("Test1"); // Simulate a project change @@ -75,7 +75,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem var configuration = Mock.Of(); // Act - ProjectManager.ProjectChanged(new ProjectSnapshotUpdateContext(project) { Configuration = configuration }); + ProjectManager.ProjectUpdated(new ProjectSnapshotUpdateContext(project) { Configuration = configuration }); // Assert var snapshot = ProjectManager.GetSnapshot(project.Id); @@ -95,7 +95,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem ProjectManager.Reset(); var configuration = Mock.Of(); - ProjectManager.ProjectChanged(new ProjectSnapshotUpdateContext(project) { Configuration = configuration }); + ProjectManager.ProjectUpdated(new ProjectSnapshotUpdateContext(project) { Configuration = configuration }); ProjectManager.Reset(); project = project.WithAssemblyName("Test1"); // Simulate a project change @@ -103,7 +103,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem ProjectManager.Reset(); // Act - ProjectManager.ProjectChanged(new ProjectSnapshotUpdateContext(project) { Configuration = configuration }); + ProjectManager.ProjectUpdated(new ProjectSnapshotUpdateContext(project) { Configuration = configuration }); // Assert var snapshot = ProjectManager.GetSnapshot(project.Id); @@ -132,7 +132,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem ProjectManager.Reset(); // Act - ProjectManager.ProjectChanged(update); + ProjectManager.ProjectUpdated(update); // Assert var snapshot = ProjectManager.GetSnapshot(project.Id); @@ -152,7 +152,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem ProjectManager.Reset(); var configuration = Mock.Of(); - ProjectManager.ProjectChanged(new ProjectSnapshotUpdateContext(project) { Configuration = configuration }); + ProjectManager.ProjectUpdated(new ProjectSnapshotUpdateContext(project) { Configuration = configuration }); project = project.WithAssemblyName("Test1"); // Simulate a project change ProjectManager.ProjectChanged(project); @@ -166,7 +166,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem ProjectManager.Reset(); // Act - ProjectManager.ProjectChanged(update); // Still dirty because the project changed while computing the update + ProjectManager.ProjectUpdated(update); // Still dirty because the project changed while computing the update // Assert var snapshot = ProjectManager.GetSnapshot(project.Id); @@ -200,7 +200,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem var project = Workspace.CurrentSolution.AddProject("Test", "Test", LanguageNames.CSharp); // Act - ProjectManager.ProjectChanged(new ProjectSnapshotUpdateContext(project)); + ProjectManager.ProjectUpdated(new ProjectSnapshotUpdateContext(project)); // Assert Assert.Empty(ProjectManager.Projects); @@ -266,7 +266,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem private class TestProjectSnapshotManager : DefaultProjectSnapshotManager { public TestProjectSnapshotManager(IEnumerable triggers, Workspace workspace) - : base(triggers, workspace) + : base(Mock.Of(), Mock.Of(), Mock.Of(), triggers, workspace) { } @@ -290,7 +290,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem ListenersNotified = true; } - protected override void NotifyBackgroundWorker() + protected override void NotifyBackgroundWorker(Project project) { WorkerStarted = true; } diff --git a/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/ProjectSystem/WorkspaceProjectSnapshotChangeTriggerTest.cs b/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/ProjectSystem/WorkspaceProjectSnapshotChangeTriggerTest.cs index 71a682abc9..c458b5388c 100644 --- a/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/ProjectSystem/WorkspaceProjectSnapshotChangeTriggerTest.cs +++ b/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/ProjectSystem/WorkspaceProjectSnapshotChangeTriggerTest.cs @@ -1,7 +1,11 @@ // 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.Collections.Generic; using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Moq; using Xunit; namespace Microsoft.CodeAnalysis.Razor.ProjectSystem @@ -45,7 +49,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem { // Arrange var trigger = new WorkspaceProjectSnapshotChangeTrigger(); - var projectManager = new DefaultProjectSnapshotManager(new[] { trigger }, Workspace); + var projectManager = new TestProjectSnapshotManager(new[] { trigger }, Workspace); var e = new WorkspaceChangeEventArgs(kind, oldSolution: EmptySolution, newSolution: SolutionWithTwoProjects); @@ -69,7 +73,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem { // Arrange var trigger = new WorkspaceProjectSnapshotChangeTrigger(); - var projectManager = new DefaultProjectSnapshotManager(new[] { trigger }, Workspace); + var projectManager = new TestProjectSnapshotManager(new[] { trigger }, Workspace); // Initialize with a project. This will get removed. var e = new WorkspaceChangeEventArgs(WorkspaceChangeKind.SolutionAdded, oldSolution: EmptySolution, newSolution: SolutionWithOneProject); @@ -94,7 +98,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem { // Arrange var trigger = new WorkspaceProjectSnapshotChangeTrigger(); - var projectManager = new DefaultProjectSnapshotManager(new[] { trigger }, Workspace); + var projectManager = new TestProjectSnapshotManager(new[] { trigger }, Workspace); // Initialize with some projects. var e = new WorkspaceChangeEventArgs(WorkspaceChangeKind.SolutionAdded, oldSolution: EmptySolution, newSolution: SolutionWithTwoProjects); @@ -122,7 +126,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem { // Arrange var trigger = new WorkspaceProjectSnapshotChangeTrigger(); - var projectManager = new DefaultProjectSnapshotManager(new[] { trigger }, Workspace); + var projectManager = new TestProjectSnapshotManager(new[] { trigger }, Workspace); // Initialize with some projects project. var e = new WorkspaceChangeEventArgs(WorkspaceChangeKind.SolutionAdded, oldSolution: EmptySolution, newSolution: SolutionWithTwoProjects); @@ -145,7 +149,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem { // Arrange var trigger = new WorkspaceProjectSnapshotChangeTrigger(); - var projectManager = new DefaultProjectSnapshotManager(new[] { trigger }, Workspace); + var projectManager = new TestProjectSnapshotManager(new[] { trigger }, Workspace); var solution = SolutionWithOneProject; var e = new WorkspaceChangeEventArgs(WorkspaceChangeKind.ProjectAdded, oldSolution: EmptySolution, newSolution: solution, projectId: ProjectNumberThree.Id); @@ -158,5 +162,25 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem projectManager.Projects.OrderBy(p => p.UnderlyingProject.Name), p => Assert.Equal(ProjectNumberThree.Id, p.UnderlyingProject.Id)); } + + private class TestProjectSnapshotManager : DefaultProjectSnapshotManager + { + public TestProjectSnapshotManager(IEnumerable triggers, Workspace workspace) + : base(Mock.Of(), Mock.Of(), new TestProjectSnapshotWorker(), triggers, workspace) + { + } + + protected override void NotifyBackgroundWorker(Project project) + { + } + } + + private class TestProjectSnapshotWorker : ProjectSnapshotWorker + { + public override Task ProcessUpdateAsync(ProjectSnapshotUpdateContext update, CancellationToken cancellationToken = default(CancellationToken)) + { + return Task.CompletedTask; + } + } } } diff --git a/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Editor/DefaultVisualStudioDocumentTrackerFactoryTest.cs b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Editor/DefaultVisualStudioDocumentTrackerFactoryTest.cs index 9099e2371f..ff06b4eb2b 100644 --- a/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Editor/DefaultVisualStudioDocumentTrackerFactoryTest.cs +++ b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Editor/DefaultVisualStudioDocumentTrackerFactoryTest.cs @@ -21,9 +21,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor { private static IReadOnlyList Projects = new List(); - private ProjectSnapshotManager ProjectManager { get; } = Mock.Of( - p => p.Projects == Projects && - p.Subscribe() == Mock.Of()); + private ProjectSnapshotManager ProjectManager { get; } = Mock.Of(p => p.Projects == Projects); private TextBufferProjectService ProjectService { get; } = Mock.Of( s => s.GetHierarchy(It.IsAny()) == Mock.Of() && diff --git a/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/ProjectSystem/DefaultProjectSnapshotWorkerTest.cs b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/ProjectSystem/DefaultProjectSnapshotWorkerTest.cs new file mode 100644 index 0000000000..1ab2f64753 --- /dev/null +++ b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/ProjectSystem/DefaultProjectSnapshotWorkerTest.cs @@ -0,0 +1,53 @@ +// 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; +using System.Threading.Tasks; +using Moq; +using Xunit; + +namespace Microsoft.CodeAnalysis.Razor.ProjectSystem +{ + public class DefaultProjectSnapshotWorkerTest : ForegroundDispatcherTestBase + { + public DefaultProjectSnapshotWorkerTest() + { + Project = new AdhocWorkspace().AddProject("Test1", LanguageNames.CSharp); + + CompletionSource = new TaskCompletionSource(); + ConfigurationFactory = Mock.Of(f => f.GetConfigurationAsync(It.IsAny(), default(CancellationToken)) == CompletionSource.Task); + } + + private Project Project { get; } + + private ProjectExtensibilityConfigurationFactory ConfigurationFactory { get; } + + private TaskCompletionSource CompletionSource { get; } + + [ForegroundFact] + public async Task ProcessUpdateAsync_DoesntBlockForegroundThread() + { + // Arrange + var worker = new DefaultProjectSnapshotWorker(Dispatcher, ConfigurationFactory); + + var context = new ProjectSnapshotUpdateContext(Project); + + var configuration = Mock.Of(); + + // Act 1 -- We want to verify that this doesn't block the main thread + var task = worker.ProcessUpdateAsync(context); + + // Assert 1 + // + // We haven't let the background task proceed yet, so this is still null. + Assert.Null(context.Configuration); + + // Act 2 - Ok let's go + CompletionSource.SetResult(configuration); + await task; + + // Assert 2 + Assert.Same(configuration, context.Configuration); + } + } +} diff --git a/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/ProjectSystem/ProjectSnapshotWorkerQueueTest.cs b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/ProjectSystem/ProjectSnapshotWorkerQueueTest.cs new file mode 100644 index 0000000000..7dc947f44e --- /dev/null +++ b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/ProjectSystem/ProjectSnapshotWorkerQueueTest.cs @@ -0,0 +1,148 @@ +// 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.Linq; +using System.Threading; +using System.Threading.Tasks; +using Moq; +using Xunit; + +namespace Microsoft.CodeAnalysis.Razor.ProjectSystem +{ + // These tests are really integration tests. There isn't a good way to unit test this functionality since + // the only thing in here is threading. + public class ProjectSnapshotWorkerQueueTest : ForegroundDispatcherTestBase + { + public ProjectSnapshotWorkerQueueTest() + { + Workspace = new AdhocWorkspace(); + + Project1 = Workspace.CurrentSolution.AddProject("Test1", "Test1", LanguageNames.CSharp); + Project2 = Workspace.CurrentSolution.AddProject("Test2", "Test2", LanguageNames.CSharp); + } + + public Project Project1 { get; } + + public Project Project2 { get; } + + public Workspace Workspace { get; } + + [ForegroundFact] + public async Task Queue_ProcessesNotifications_AndGoesBackToSleep() + { + // Arrange + var projectManager = new TestProjectSnapshotManager(Dispatcher, Workspace); + var projectWorker = new TestProjectSnapshotWorker(); + + var queue = new ProjectSnapshotWorkerQueue(Dispatcher, projectManager, projectWorker) + { + Delay = TimeSpan.FromMilliseconds(1), + BlockBackgroundWorkStart = new ManualResetEventSlim(initialState: false), + NotifyBackgroundWorkFinish = new ManualResetEventSlim(initialState: false), + NotifyForegroundWorkFinish = new ManualResetEventSlim(initialState: false), + }; + + // Act & Assert + queue.Enqueue(Project1); + + Assert.True(queue.IsScheduledOrRunning); + Assert.True(queue.HasPendingNotifications); + + // Allow the background work to proceed. + queue.BlockBackgroundWorkStart.Set(); + + // Get off the foreground thread and allow the updates to flow through. + await Task.Run(() => queue.NotifyForegroundWorkFinish.Wait(TimeSpan.FromSeconds(1))); + + Assert.False(queue.IsScheduledOrRunning); + Assert.False(queue.HasPendingNotifications); + } + + [ForegroundFact] + public async Task Queue_ProcessesNotifications_AndRestarts() + { + // Arrange + var projectManager = new TestProjectSnapshotManager(Dispatcher, Workspace); + var projectWorker = new TestProjectSnapshotWorker(); + + var queue = new ProjectSnapshotWorkerQueue(Dispatcher, projectManager, projectWorker) + { + Delay = TimeSpan.FromMilliseconds(1), + BlockBackgroundWorkStart = new ManualResetEventSlim(initialState: false), + NotifyBackgroundWorkFinish = new ManualResetEventSlim(initialState: false), + NotifyForegroundWorkFinish = new ManualResetEventSlim(initialState: false), + }; + + // Act & Assert + queue.Enqueue(Project1); + + Assert.True(queue.IsScheduledOrRunning); + Assert.True(queue.HasPendingNotifications); + + // Allow the background work to proceed. + queue.BlockBackgroundWorkStart.Set(); + + queue.NotifyBackgroundWorkFinish.Wait(); // Block the foreground thread so we can queue another notification. + + Assert.True(queue.IsScheduledOrRunning); + Assert.False(queue.HasPendingNotifications); + + queue.Enqueue(Project2); + + Assert.True(queue.HasPendingNotifications); // Now we should see the worker restart when it finishes. + + // Get off the foreground thread and allow the updates to flow through. + await Task.Run(() => queue.NotifyForegroundWorkFinish.Wait(TimeSpan.FromSeconds(1))); + + queue.NotifyBackgroundWorkFinish.Reset(); + queue.NotifyForegroundWorkFinish.Reset(); + + // It should start running again right away. + Assert.True(queue.IsScheduledOrRunning); + Assert.True(queue.HasPendingNotifications); + + // Allow the background work to proceed. + queue.BlockBackgroundWorkStart.Set(); + + // Get off the foreground thread and allow the updates to flow through. + await Task.Run(() => queue.NotifyForegroundWorkFinish.Wait(TimeSpan.FromSeconds(1))); + + Assert.False(queue.IsScheduledOrRunning); + Assert.False(queue.HasPendingNotifications); + } + + private class TestProjectSnapshotManager : DefaultProjectSnapshotManager + { + public TestProjectSnapshotManager(ForegroundDispatcher foregroundDispatcher, Workspace workspace) + : base(foregroundDispatcher, Mock.Of(), new TestProjectSnapshotWorker(), Enumerable.Empty(), workspace) + { + } + + public DefaultProjectSnapshot GetSnapshot(ProjectId id) + { + return Projects.Cast().FirstOrDefault(s => s.UnderlyingProject.Id == id); + } + + protected override void NotifyListeners(ProjectChangeEventArgs e) + { + } + + protected override void NotifyBackgroundWorker(Project project) + { + } + } + + private class TestProjectSnapshotWorker : ProjectSnapshotWorker + { + public TestProjectSnapshotWorker() + { + } + + public override Task ProcessUpdateAsync(ProjectSnapshotUpdateContext update, CancellationToken cancellationToken = default(CancellationToken)) + { + return Task.CompletedTask; + } + } + } +} diff --git a/tooling/Microsoft.VisualStudio.RazorExtension/DocumentInfo/RazorDocumentInfoViewModel.cs b/tooling/Microsoft.VisualStudio.RazorExtension/DocumentInfo/RazorDocumentInfoViewModel.cs index a3c8e85efb..b4bbc808c3 100644 --- a/tooling/Microsoft.VisualStudio.RazorExtension/DocumentInfo/RazorDocumentInfoViewModel.cs +++ b/tooling/Microsoft.VisualStudio.RazorExtension/DocumentInfo/RazorDocumentInfoViewModel.cs @@ -5,8 +5,6 @@ using System; using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Host; -using Microsoft.CodeAnalysis.Razor; using Microsoft.VisualStudio.LanguageServices.Razor.Editor; namespace Microsoft.VisualStudio.RazorExtension.DocumentInfo @@ -25,6 +23,8 @@ namespace Microsoft.VisualStudio.RazorExtension.DocumentInfo _documentTracker = documentTracker; } + public string Configuration => _documentTracker.Configuration?.DisplayName; + public bool IsSupportedDocument => _documentTracker.IsSupportedProject; public Project Project @@ -43,18 +43,6 @@ namespace Microsoft.VisualStudio.RazorExtension.DocumentInfo public ProjectId ProjectId => _documentTracker.Project?.Id; public Workspace Workspace => _documentTracker.Workspace; - - public HostLanguageServices RazorLanguageServices => Workspace?.Services.GetLanguageServices(RazorLanguage.Name); - - public TagHelperResolver TagHelperResolver => RazorLanguageServices?.GetRequiredService(); - - public RazorSyntaxFactsService RazorSyntaxFactsService => RazorLanguageServices?.GetRequiredService(); - - public RazorTemplateEngineFactoryService RazorTemplateEngineFactoryService => RazorLanguageServices?.GetRequiredService(); - - public TagHelperCompletionService TagHelperCompletionService => RazorLanguageServices?.GetRequiredService(); - - public TagHelperFactsService TagHelperFactsService => RazorLanguageServices?.GetRequiredService(); } } diff --git a/tooling/Microsoft.VisualStudio.RazorExtension/DocumentInfo/RazorDocumentInfoWindowControl.xaml b/tooling/Microsoft.VisualStudio.RazorExtension/DocumentInfo/RazorDocumentInfoWindowControl.xaml index 123118bd34..86a4064fa6 100644 --- a/tooling/Microsoft.VisualStudio.RazorExtension/DocumentInfo/RazorDocumentInfoWindowControl.xaml +++ b/tooling/Microsoft.VisualStudio.RazorExtension/DocumentInfo/RazorDocumentInfoWindowControl.xaml @@ -24,8 +24,8 @@