Add a background listener for notifications

This change adds an actual background worker for listening to project
change notifications and starts sending updates when the project's razor
dependencies change.

I had to do a litle surgery to get things working. There were plenty of
small bug fixes.

Additionally I got rid of the WeakReferences for tracking listeners. I
was seeing TextBuffers hanging around in VS longer than I expected and
the WeakReferences weren't getting cleaned up. I think it's better that
we just track the lifetime.
This commit is contained in:
Ryan Nowak 2017-09-05 19:36:26 -07:00
parent 7a0abc3f67
commit 6e6a24cbb4
31 changed files with 872 additions and 153 deletions

View File

@ -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.
}
}
}

View File

@ -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();
}
}
}

View File

@ -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);
}
}

View File

@ -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<ProjectChangeEventArgs> ProjectChanged;
internal void Notify(ProjectChangeEventArgs e)
{
var handler = ProjectChanged;
if (handler != null)
{
handler(this, e);
}
}
}
}

View File

@ -9,12 +9,38 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
{ {
internal class DefaultProjectSnapshotManager : ProjectSnapshotManagerBase internal class DefaultProjectSnapshotManager : ProjectSnapshotManagerBase
{ {
private readonly ProjectSnapshotChangeTrigger[] _triggers; public override event EventHandler<ProjectChangeEventArgs> Changed;
private readonly Dictionary<ProjectId, DefaultProjectSnapshot> _projects;
private readonly List<WeakReference<DefaultProjectSnapshotListener>> _listeners;
public DefaultProjectSnapshotManager(IEnumerable<ProjectSnapshotChangeTrigger> 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<ProjectId, DefaultProjectSnapshot> _projects;
public DefaultProjectSnapshotManager(
ForegroundDispatcher foregroundDispatcher,
ErrorReporter errorReporter,
ProjectSnapshotWorker worker,
IEnumerable<ProjectSnapshotChangeTrigger> 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) if (triggers == null)
{ {
throw new ArgumentNullException(nameof(triggers)); throw new ArgumentNullException(nameof(triggers));
@ -25,11 +51,14 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
throw new ArgumentNullException(nameof(workspace)); throw new ArgumentNullException(nameof(workspace));
} }
_foregroundDispatcher = foregroundDispatcher;
_errorReporter = errorReporter;
_worker = worker;
_triggers = triggers.ToArray(); _triggers = triggers.ToArray();
Workspace = workspace; Workspace = workspace;
_projects = new Dictionary<ProjectId, DefaultProjectSnapshot>(); _projects = new Dictionary<ProjectId, DefaultProjectSnapshot>();
_listeners = new List<WeakReference<DefaultProjectSnapshotListener>>(); _workerQueue = new ProjectSnapshotWorkerQueue(_foregroundDispatcher, this, worker);
for (var i = 0; i < _triggers.Length; i++) for (var i = 0; i < _triggers.Length; i++)
{ {
@ -37,18 +66,27 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
} }
} }
public override IReadOnlyList<ProjectSnapshot> Projects => _projects.Values.ToArray(); public override IReadOnlyList<ProjectSnapshot> 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 Workspace Workspace { get; }
public override ProjectSnapshotListener Subscribe()
{
var subscription = new DefaultProjectSnapshotListener();
_listeners.Add(new WeakReference<DefaultProjectSnapshotListener>(subscription));
return subscription;
}
public override void ProjectAdded(Project underlyingProject) public override void ProjectAdded(Project underlyingProject)
{ {
if (underlyingProject == null) if (underlyingProject == null)
@ -60,7 +98,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
_projects[underlyingProject.Id] = snapshot; _projects[underlyingProject.Id] = snapshot;
// New projects always start dirty, need to compute state in the background. // 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. // We need to notify listeners about every project add.
NotifyListeners(new ProjectChangeEventArgs(snapshot, ProjectChangeKind.Added)); 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 // 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. // 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) 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 // 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. // 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 // 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 // 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 // virtual so it can be overridden in tests
protected virtual void NotifyListeners(ProjectChangeEventArgs e) 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)) handler(this, e);
{
listener.Notify(e);
}
else
{
_listeners.RemoveAt(i--);
}
} }
} }
public override void ReportError(Exception exception)
{
_errorReporter.ReportError(exception);
}
public override void ReportError(Exception exception, Project project)
{
_errorReporter.ReportError(exception, project);
}
} }
} }

View File

@ -16,7 +16,8 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
private readonly IEnumerable<ProjectSnapshotChangeTrigger> _triggers; private readonly IEnumerable<ProjectSnapshotChangeTrigger> _triggers;
[ImportingConstructor] [ImportingConstructor]
public DefaultProjectSnapshotManagerFactory([ImportMany] IEnumerable<ProjectSnapshotChangeTrigger> triggers) public DefaultProjectSnapshotManagerFactory(
[ImportMany] IEnumerable<ProjectSnapshotChangeTrigger> triggers)
{ {
_triggers = triggers; _triggers = triggers;
} }
@ -28,7 +29,12 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
throw new ArgumentNullException(nameof(languageServices)); throw new ArgumentNullException(nameof(languageServices));
} }
return new DefaultProjectSnapshotManager(_triggers, languageServices.WorkspaceServices.Workspace); return new DefaultProjectSnapshotManager(
languageServices.WorkspaceServices.GetRequiredService<ForegroundDispatcher>(),
languageServices.WorkspaceServices.GetRequiredService<ErrorReporter>(),
languageServices.GetRequiredService<ProjectSnapshotWorker>(),
_triggers,
languageServices.WorkspaceServices.Workspace);
} }
} }
} }

View File

@ -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;
}
}
}

View File

@ -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<ForegroundDispatcher>(),
languageServices.GetRequiredService<ProjectExtensibilityConfigurationFactory>());
}
}
}

View File

@ -34,6 +34,9 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
public override IReadOnlyList<ProjectExtensibilityAssembly> Assemblies { get; } public override IReadOnlyList<ProjectExtensibilityAssembly> 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 ProjectExtensibilityConfigurationKind Kind { get; }
public override ProjectExtensibilityAssembly RazorAssembly { 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. // 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() public override int GetHashCode()
@ -61,5 +67,10 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
return hash; return hash;
} }
public override string ToString()
{
return DisplayName;
}
} }
} }

View File

@ -10,6 +10,8 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
{ {
public abstract IReadOnlyList<ProjectExtensibilityAssembly> Assemblies { get; } public abstract IReadOnlyList<ProjectExtensibilityAssembly> Assemblies { get; }
public abstract string DisplayName { get; }
public abstract ProjectExtensibilityConfigurationKind Kind { get; } public abstract ProjectExtensibilityConfigurationKind Kind { get; }
public abstract ProjectExtensibilityAssembly RazorAssembly { get; } public abstract ProjectExtensibilityAssembly RazorAssembly { get; }
@ -20,7 +22,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
public override bool Equals(object obj) public override bool Equals(object obj)
{ {
return base.Equals(obj as ProjectExtensibilityConfiguration); return Equals(obj as ProjectExtensibilityConfiguration);
} }
} }
} }

View File

@ -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<ProjectChangeEventArgs> ProjectChanged;
}
}

View File

@ -1,6 +1,7 @@
// Copyright (c) .NET Foundation. All rights reserved. // 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic; using System.Collections.Generic;
using Microsoft.CodeAnalysis.Host; using Microsoft.CodeAnalysis.Host;
@ -8,8 +9,9 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
{ {
internal abstract class ProjectSnapshotManager : ILanguageService internal abstract class ProjectSnapshotManager : ILanguageService
{ {
public abstract event EventHandler<ProjectChangeEventArgs> Changed;
public abstract IReadOnlyList<ProjectSnapshot> Projects { get; } public abstract IReadOnlyList<ProjectSnapshot> Projects { get; }
public abstract ProjectSnapshotListener Subscribe();
} }
} }

View File

@ -1,6 +1,8 @@
// Copyright (c) .NET Foundation. All rights reserved. // 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. // 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 namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
{ {
internal abstract class ProjectSnapshotManagerBase : ProjectSnapshotManager internal abstract class ProjectSnapshotManagerBase : ProjectSnapshotManager
@ -11,10 +13,14 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
public abstract void ProjectChanged(Project underlyingProject); 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 ProjectRemoved(Project underlyingProject);
public abstract void ProjectsCleared(); public abstract void ProjectsCleared();
public abstract void ReportError(Exception exception);
public abstract void ReportError(Exception exception, Project project);
} }
} }

View File

@ -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));
}
}

View File

@ -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<ProjectId, Project> _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<ProjectId, Project>();
}
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);
}
}
}
}
}

View File

@ -27,7 +27,10 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
foreach (var project in solution.Projects) 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); underlyingProject = e.NewSolution.GetProject(e.ProjectId);
Debug.Assert(underlyingProject != null); Debug.Assert(underlyingProject != null);
_projectManager.ProjectAdded(underlyingProject); if (underlyingProject.Language == LanguageNames.CSharp)
{
_projectManager.ProjectAdded(underlyingProject);
}
break; break;
} }

View File

@ -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<AssemblyIdentity>
{
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;
}
}
}
}

View File

@ -19,17 +19,22 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor
{ {
internal class DefaultTagHelperResolver : TagHelperResolver internal class DefaultTagHelperResolver : TagHelperResolver
{ {
private readonly ErrorReporter _errorReporter;
private readonly Workspace _workspace; private readonly Workspace _workspace;
private readonly IServiceProvider _services;
public DefaultTagHelperResolver(Workspace workspace, IServiceProvider services) public DefaultTagHelperResolver(ErrorReporter errorReporter, Workspace workspace)
{ {
_errorReporter = errorReporter;
_workspace = workspace; _workspace = workspace;
_services = services;
} }
public async Task<TagHelperResolutionResult> GetTagHelpersAsync(Project project) public async Task<TagHelperResolutionResult> GetTagHelpersAsync(Project project)
{ {
if (project == null)
{
throw new ArgumentNullException(nameof(project));
}
try try
{ {
TagHelperResolutionResult result; TagHelperResolutionResult result;
@ -67,15 +72,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor
} }
catch (Exception exception) catch (Exception exception)
{ {
var log = GetActivityLog(); _errorReporter.ReportError(exception, project);
if (log != null)
{
var hr = log.LogEntry(
(uint)__ACTIVITYLOG_ENTRYTYPE.ALE_ERROR,
"Razor Language Services",
$"Error discovering TagHelpers:{Environment.NewLine}{exception}");
ErrorHandler.ThrowOnFailure(hr);
}
throw new RazorLanguageServiceException( throw new RazorLanguageServiceException(
typeof(DefaultTagHelperResolver).FullName, typeof(DefaultTagHelperResolver).FullName,
@ -121,10 +118,5 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor
return serializer.Deserialize<TagHelperResolutionResult>(reader); return serializer.Deserialize<TagHelperResolutionResult>(reader);
} }
} }
private IVsActivityLog GetActivityLog()
{
return _services.GetService(typeof(SVsActivityLog)) as IVsActivityLog;
}
} }
} }

View File

@ -5,7 +5,6 @@ using System.ComponentModel.Composition;
using Microsoft.CodeAnalysis.Host; using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.Razor; using Microsoft.CodeAnalysis.Razor;
using Microsoft.VisualStudio.Shell;
namespace Microsoft.VisualStudio.LanguageServices.Razor namespace Microsoft.VisualStudio.LanguageServices.Razor
{ {
@ -15,12 +14,9 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor
[Import] [Import]
public VisualStudioWorkspace Workspace { get; set; } public VisualStudioWorkspace Workspace { get; set; }
[Import]
public SVsServiceProvider Services { get; set; }
public ILanguageService CreateLanguageService(HostLanguageServices languageServices) public ILanguageService CreateLanguageService(HostLanguageServices languageServices)
{ {
return new DefaultTagHelperResolver(Workspace, Services); return new DefaultTagHelperResolver(Workspace.Services.GetRequiredService<ErrorReporter>(), Workspace);
} }
} }
} }

View File

@ -23,7 +23,6 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor
private bool _isSupportedProject; private bool _isSupportedProject;
private ProjectSnapshot _project; private ProjectSnapshot _project;
private string _projectPath; private string _projectPath;
private ProjectSnapshotListener _subscription;
public override event EventHandler ContextChanged; 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. _workspace = workspace; // For now we assume that the workspace is the always default VS workspace.
_textViews = new List<ITextView>(); _textViews = new List<ITextView>();
Initialize();
} }
internal override ProjectExtensibilityConfiguration Configuration => _project.Configuration;
public override bool IsSupportedProject => _isSupportedProject; 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; public override ITextBuffer TextBuffer => _textBuffer;
@ -75,7 +74,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor
public override Workspace Workspace => _workspace; 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 // 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 // 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; return;
} }
var project = _projectManager.GetProjectWithFilePath(projectPath);
var subscription = _projectManager.Subscribe();
subscription.ProjectChanged += Subscription_ProjectStateChanged;
_isSupportedProject = isSupportedProject; _isSupportedProject = isSupportedProject;
_projectPath = projectPath; _projectPath = projectPath;
_project = project; _project = _projectManager.GetProjectWithFilePath(projectPath);
_subscription = subscription; _projectManager.Changed += ProjectManager_Changed;
OnContextChanged(_project);
} }
public void Unsubscribe()
{
_projectManager.Changed -= ProjectManager_Changed;
}
private void OnContextChanged(ProjectSnapshot project) private void OnContextChanged(ProjectSnapshot project)
{ {
_project = 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 && if (_projectPath != null &&
string.Equals(_projectPath, e.Project.UnderlyingProject.FilePath, StringComparison.OrdinalIgnoreCase)) string.Equals(_projectPath, e.Project.UnderlyingProject.FilePath, StringComparison.OrdinalIgnoreCase))

View File

@ -28,15 +28,9 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor
[ImportingConstructor] [ImportingConstructor]
public DefaultVisualStudioDocumentTrackerFactory( public DefaultVisualStudioDocumentTrackerFactory(
ForegroundDispatcher foregroundDispatcher,
TextBufferProjectService projectService, TextBufferProjectService projectService,
[Import(typeof(VisualStudioWorkspace))] Workspace workspace) [Import(typeof(VisualStudioWorkspace))] Workspace workspace)
{ {
if (foregroundDispatcher == null)
{
throw new ArgumentNullException(nameof(foregroundDispatcher));
}
if (projectService == null) if (projectService == null)
{ {
throw new ArgumentNullException(nameof(projectService)); throw new ArgumentNullException(nameof(projectService));
@ -46,11 +40,11 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor
{ {
throw new ArgumentNullException(nameof(workspace)); throw new ArgumentNullException(nameof(workspace));
} }
_foregroundDispatcher = foregroundDispatcher;
_projectService = projectService; _projectService = projectService;
_workspace = workspace; _workspace = workspace;
_foregroundDispatcher = workspace.Services.GetRequiredService<ForegroundDispatcher>();
_projectManager = workspace.Services.GetLanguageServices(RazorLanguage.Name).GetRequiredService<ProjectSnapshotManager>(); _projectManager = workspace.Services.GetLanguageServices(RazorLanguage.Name).GetRequiredService<ProjectSnapshotManager>();
} }
@ -151,6 +145,10 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor
if (!tracker.TextViewsInternal.Contains(textView)) if (!tracker.TextViewsInternal.Contains(textView))
{ {
tracker.TextViewsInternal.Add(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)) if (textBuffer.Properties.TryGetProperty(typeof(VisualStudioDocumentTracker), out tracker))
{ {
tracker.TextViewsInternal.Remove(textView); tracker.TextViewsInternal.Remove(textView);
if (tracker.TextViewsInternal.Count == 0)
{
tracker.Unsubscribe();
}
} }
} }
} }

View File

@ -4,6 +4,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
using Microsoft.VisualStudio.Text; using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Editor; using Microsoft.VisualStudio.Text.Editor;
@ -13,6 +14,8 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor
{ {
public abstract event EventHandler ContextChanged; public abstract event EventHandler ContextChanged;
internal abstract ProjectExtensibilityConfiguration Configuration { get; }
public abstract bool IsSupportedProject { get; } public abstract bool IsSupportedProject { get; }
public abstract Project Project { get; } public abstract Project Project { get; }

View File

@ -1,10 +1,9 @@
// Copyright (c) .NET Foundation. All rights reserved. // 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. // 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 System.ComponentModel.Composition;
using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis;
using Microsoft.VisualStudio.Shell; using Microsoft.CodeAnalysis.Razor;
namespace Microsoft.VisualStudio.LanguageServices.Razor namespace Microsoft.VisualStudio.LanguageServices.Razor
{ {
@ -17,9 +16,8 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor
{ {
[ImportingConstructor] [ImportingConstructor]
public LegacyTagHelperResolver( public LegacyTagHelperResolver(
[Import(typeof(VisualStudioWorkspace))] Workspace workspace, [Import(typeof(VisualStudioWorkspace))] Workspace workspace)
[Import(typeof(SVsServiceProvider))] IServiceProvider services) : : base(workspace.Services.GetRequiredService<ErrorReporter>(), workspace)
base(workspace, services)
{ {
} }
} }

View File

@ -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;
}
}
}

View File

@ -47,7 +47,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
// Adding some computed state // Adding some computed state
var configuration = Mock.Of<ProjectExtensibilityConfiguration>(); var configuration = Mock.Of<ProjectExtensibilityConfiguration>();
ProjectManager.ProjectChanged(new ProjectSnapshotUpdateContext(project) { Configuration = configuration }); ProjectManager.ProjectUpdated(new ProjectSnapshotUpdateContext(project) { Configuration = configuration });
ProjectManager.Reset(); ProjectManager.Reset();
project = project.WithAssemblyName("Test1"); // Simulate a project change project = project.WithAssemblyName("Test1"); // Simulate a project change
@ -75,7 +75,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
var configuration = Mock.Of<ProjectExtensibilityConfiguration>(); var configuration = Mock.Of<ProjectExtensibilityConfiguration>();
// Act // Act
ProjectManager.ProjectChanged(new ProjectSnapshotUpdateContext(project) { Configuration = configuration }); ProjectManager.ProjectUpdated(new ProjectSnapshotUpdateContext(project) { Configuration = configuration });
// Assert // Assert
var snapshot = ProjectManager.GetSnapshot(project.Id); var snapshot = ProjectManager.GetSnapshot(project.Id);
@ -95,7 +95,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
ProjectManager.Reset(); ProjectManager.Reset();
var configuration = Mock.Of<ProjectExtensibilityConfiguration>(); var configuration = Mock.Of<ProjectExtensibilityConfiguration>();
ProjectManager.ProjectChanged(new ProjectSnapshotUpdateContext(project) { Configuration = configuration }); ProjectManager.ProjectUpdated(new ProjectSnapshotUpdateContext(project) { Configuration = configuration });
ProjectManager.Reset(); ProjectManager.Reset();
project = project.WithAssemblyName("Test1"); // Simulate a project change project = project.WithAssemblyName("Test1"); // Simulate a project change
@ -103,7 +103,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
ProjectManager.Reset(); ProjectManager.Reset();
// Act // Act
ProjectManager.ProjectChanged(new ProjectSnapshotUpdateContext(project) { Configuration = configuration }); ProjectManager.ProjectUpdated(new ProjectSnapshotUpdateContext(project) { Configuration = configuration });
// Assert // Assert
var snapshot = ProjectManager.GetSnapshot(project.Id); var snapshot = ProjectManager.GetSnapshot(project.Id);
@ -132,7 +132,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
ProjectManager.Reset(); ProjectManager.Reset();
// Act // Act
ProjectManager.ProjectChanged(update); ProjectManager.ProjectUpdated(update);
// Assert // Assert
var snapshot = ProjectManager.GetSnapshot(project.Id); var snapshot = ProjectManager.GetSnapshot(project.Id);
@ -152,7 +152,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
ProjectManager.Reset(); ProjectManager.Reset();
var configuration = Mock.Of<ProjectExtensibilityConfiguration>(); var configuration = Mock.Of<ProjectExtensibilityConfiguration>();
ProjectManager.ProjectChanged(new ProjectSnapshotUpdateContext(project) { Configuration = configuration }); ProjectManager.ProjectUpdated(new ProjectSnapshotUpdateContext(project) { Configuration = configuration });
project = project.WithAssemblyName("Test1"); // Simulate a project change project = project.WithAssemblyName("Test1"); // Simulate a project change
ProjectManager.ProjectChanged(project); ProjectManager.ProjectChanged(project);
@ -166,7 +166,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
ProjectManager.Reset(); ProjectManager.Reset();
// Act // 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 // Assert
var snapshot = ProjectManager.GetSnapshot(project.Id); var snapshot = ProjectManager.GetSnapshot(project.Id);
@ -200,7 +200,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
var project = Workspace.CurrentSolution.AddProject("Test", "Test", LanguageNames.CSharp); var project = Workspace.CurrentSolution.AddProject("Test", "Test", LanguageNames.CSharp);
// Act // Act
ProjectManager.ProjectChanged(new ProjectSnapshotUpdateContext(project)); ProjectManager.ProjectUpdated(new ProjectSnapshotUpdateContext(project));
// Assert // Assert
Assert.Empty(ProjectManager.Projects); Assert.Empty(ProjectManager.Projects);
@ -266,7 +266,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
private class TestProjectSnapshotManager : DefaultProjectSnapshotManager private class TestProjectSnapshotManager : DefaultProjectSnapshotManager
{ {
public TestProjectSnapshotManager(IEnumerable<ProjectSnapshotChangeTrigger> triggers, Workspace workspace) public TestProjectSnapshotManager(IEnumerable<ProjectSnapshotChangeTrigger> triggers, Workspace workspace)
: base(triggers, workspace) : base(Mock.Of<ForegroundDispatcher>(), Mock.Of<ErrorReporter>(), Mock.Of<ProjectSnapshotWorker>(), triggers, workspace)
{ {
} }
@ -290,7 +290,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
ListenersNotified = true; ListenersNotified = true;
} }
protected override void NotifyBackgroundWorker() protected override void NotifyBackgroundWorker(Project project)
{ {
WorkerStarted = true; WorkerStarted = true;
} }

View File

@ -1,7 +1,11 @@
// Copyright (c) .NET Foundation. All rights reserved. // 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. // 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.Linq;
using System.Threading;
using System.Threading.Tasks;
using Moq;
using Xunit; using Xunit;
namespace Microsoft.CodeAnalysis.Razor.ProjectSystem namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
@ -45,7 +49,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
{ {
// Arrange // Arrange
var trigger = new WorkspaceProjectSnapshotChangeTrigger(); 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); var e = new WorkspaceChangeEventArgs(kind, oldSolution: EmptySolution, newSolution: SolutionWithTwoProjects);
@ -69,7 +73,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
{ {
// Arrange // Arrange
var trigger = new WorkspaceProjectSnapshotChangeTrigger(); 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. // Initialize with a project. This will get removed.
var e = new WorkspaceChangeEventArgs(WorkspaceChangeKind.SolutionAdded, oldSolution: EmptySolution, newSolution: SolutionWithOneProject); var e = new WorkspaceChangeEventArgs(WorkspaceChangeKind.SolutionAdded, oldSolution: EmptySolution, newSolution: SolutionWithOneProject);
@ -94,7 +98,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
{ {
// Arrange // Arrange
var trigger = new WorkspaceProjectSnapshotChangeTrigger(); var trigger = new WorkspaceProjectSnapshotChangeTrigger();
var projectManager = new DefaultProjectSnapshotManager(new[] { trigger }, Workspace); var projectManager = new TestProjectSnapshotManager(new[] { trigger }, Workspace);
// Initialize with some projects. // Initialize with some projects.
var e = new WorkspaceChangeEventArgs(WorkspaceChangeKind.SolutionAdded, oldSolution: EmptySolution, newSolution: SolutionWithTwoProjects); var e = new WorkspaceChangeEventArgs(WorkspaceChangeKind.SolutionAdded, oldSolution: EmptySolution, newSolution: SolutionWithTwoProjects);
@ -122,7 +126,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
{ {
// Arrange // Arrange
var trigger = new WorkspaceProjectSnapshotChangeTrigger(); var trigger = new WorkspaceProjectSnapshotChangeTrigger();
var projectManager = new DefaultProjectSnapshotManager(new[] { trigger }, Workspace); var projectManager = new TestProjectSnapshotManager(new[] { trigger }, Workspace);
// Initialize with some projects project. // Initialize with some projects project.
var e = new WorkspaceChangeEventArgs(WorkspaceChangeKind.SolutionAdded, oldSolution: EmptySolution, newSolution: SolutionWithTwoProjects); var e = new WorkspaceChangeEventArgs(WorkspaceChangeKind.SolutionAdded, oldSolution: EmptySolution, newSolution: SolutionWithTwoProjects);
@ -145,7 +149,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
{ {
// Arrange // Arrange
var trigger = new WorkspaceProjectSnapshotChangeTrigger(); var trigger = new WorkspaceProjectSnapshotChangeTrigger();
var projectManager = new DefaultProjectSnapshotManager(new[] { trigger }, Workspace); var projectManager = new TestProjectSnapshotManager(new[] { trigger }, Workspace);
var solution = SolutionWithOneProject; var solution = SolutionWithOneProject;
var e = new WorkspaceChangeEventArgs(WorkspaceChangeKind.ProjectAdded, oldSolution: EmptySolution, newSolution: solution, projectId: ProjectNumberThree.Id); 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), projectManager.Projects.OrderBy(p => p.UnderlyingProject.Name),
p => Assert.Equal(ProjectNumberThree.Id, p.UnderlyingProject.Id)); p => Assert.Equal(ProjectNumberThree.Id, p.UnderlyingProject.Id));
} }
private class TestProjectSnapshotManager : DefaultProjectSnapshotManager
{
public TestProjectSnapshotManager(IEnumerable<ProjectSnapshotChangeTrigger> triggers, Workspace workspace)
: base(Mock.Of<ForegroundDispatcher>(), Mock.Of<ErrorReporter>(), 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;
}
}
} }
} }

View File

@ -21,9 +21,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor
{ {
private static IReadOnlyList<ProjectSnapshot> Projects = new List<ProjectSnapshot>(); private static IReadOnlyList<ProjectSnapshot> Projects = new List<ProjectSnapshot>();
private ProjectSnapshotManager ProjectManager { get; } = Mock.Of<ProjectSnapshotManager>( private ProjectSnapshotManager ProjectManager { get; } = Mock.Of<ProjectSnapshotManager>(p => p.Projects == Projects);
p => p.Projects == Projects &&
p.Subscribe() == Mock.Of<ProjectSnapshotListener>());
private TextBufferProjectService ProjectService { get; } = Mock.Of<TextBufferProjectService>( private TextBufferProjectService ProjectService { get; } = Mock.Of<TextBufferProjectService>(
s => s.GetHierarchy(It.IsAny<ITextBuffer>()) == Mock.Of<IVsHierarchy>() && s => s.GetHierarchy(It.IsAny<ITextBuffer>()) == Mock.Of<IVsHierarchy>() &&

View File

@ -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<ProjectExtensibilityConfiguration>();
ConfigurationFactory = Mock.Of<ProjectExtensibilityConfigurationFactory>(f => f.GetConfigurationAsync(It.IsAny<Project>(), default(CancellationToken)) == CompletionSource.Task);
}
private Project Project { get; }
private ProjectExtensibilityConfigurationFactory ConfigurationFactory { get; }
private TaskCompletionSource<ProjectExtensibilityConfiguration> CompletionSource { get; }
[ForegroundFact]
public async Task ProcessUpdateAsync_DoesntBlockForegroundThread()
{
// Arrange
var worker = new DefaultProjectSnapshotWorker(Dispatcher, ConfigurationFactory);
var context = new ProjectSnapshotUpdateContext(Project);
var configuration = Mock.Of<ProjectExtensibilityConfiguration>();
// 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);
}
}
}

View File

@ -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<ErrorReporter>(), new TestProjectSnapshotWorker(), Enumerable.Empty<ProjectSnapshotChangeTrigger>(), workspace)
{
}
public DefaultProjectSnapshot GetSnapshot(ProjectId id)
{
return Projects.Cast<DefaultProjectSnapshot>().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;
}
}
}
}

View File

@ -5,8 +5,6 @@
using System; using System;
using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Razor;
using Microsoft.VisualStudio.LanguageServices.Razor.Editor; using Microsoft.VisualStudio.LanguageServices.Razor.Editor;
namespace Microsoft.VisualStudio.RazorExtension.DocumentInfo namespace Microsoft.VisualStudio.RazorExtension.DocumentInfo
@ -25,6 +23,8 @@ namespace Microsoft.VisualStudio.RazorExtension.DocumentInfo
_documentTracker = documentTracker; _documentTracker = documentTracker;
} }
public string Configuration => _documentTracker.Configuration?.DisplayName;
public bool IsSupportedDocument => _documentTracker.IsSupportedProject; public bool IsSupportedDocument => _documentTracker.IsSupportedProject;
public Project Project public Project Project
@ -43,18 +43,6 @@ namespace Microsoft.VisualStudio.RazorExtension.DocumentInfo
public ProjectId ProjectId => _documentTracker.Project?.Id; public ProjectId ProjectId => _documentTracker.Project?.Id;
public Workspace Workspace => _documentTracker.Workspace; public Workspace Workspace => _documentTracker.Workspace;
public HostLanguageServices RazorLanguageServices => Workspace?.Services.GetLanguageServices(RazorLanguage.Name);
public TagHelperResolver TagHelperResolver => RazorLanguageServices?.GetRequiredService<TagHelperResolver>();
public RazorSyntaxFactsService RazorSyntaxFactsService => RazorLanguageServices?.GetRequiredService<RazorSyntaxFactsService>();
public RazorTemplateEngineFactoryService RazorTemplateEngineFactoryService => RazorLanguageServices?.GetRequiredService<RazorTemplateEngineFactoryService>();
public TagHelperCompletionService TagHelperCompletionService => RazorLanguageServices?.GetRequiredService<TagHelperCompletionService>();
public TagHelperFactsService TagHelperFactsService => RazorLanguageServices?.GetRequiredService<TagHelperFactsService>();
} }
} }

View File

@ -24,8 +24,8 @@
<Label Grid.Column="1" Grid.Row="0" Content="{Binding Project.Name}"/> <Label Grid.Column="1" Grid.Row="0" Content="{Binding Project.Name}"/>
<Label Grid.Column="0" Grid.Row="1" Content="Is Supported"/> <Label Grid.Column="0" Grid.Row="1" Content="Is Supported"/>
<Label Grid.Column="1" Grid.Row="1" Content="{Binding IsSupportedDocument}"/> <Label Grid.Column="1" Grid.Row="1" Content="{Binding IsSupportedDocument}"/>
<Label Grid.Column="0" Grid.Row="2" Content="Workspace"/> <Label Grid.Column="0" Grid.Row="2" Content="Configuration"/>
<Label Grid.Column="1" Grid.Row="2" Content="{Binding Workspace}"/> <Label Grid.Column="1" Grid.Row="2" Content="{Binding Configuration}"/>
</Grid> </Grid>
</StackPanel> </StackPanel>
</UserControl> </UserControl>