Add documents, engine, tag helpers to snapshot

The project snapshot now maintains a RazorProjectEngine as well as set
of Tag Helpers that are known for that snapshot.

Pivoted some more services to be snapshot-centric.

Also added the ability to track .cshtml documents to the project system.
For now most components just ignore document changes.
This commit is contained in:
Ryan Nowak 2018-04-11 20:52:45 -07:00 committed by Ryan Nowak
parent d8990cc2b8
commit e2edc280c5
103 changed files with 4694 additions and 2989 deletions

View File

@ -4,10 +4,11 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Extensions.Internal;
namespace Microsoft.AspNetCore.Razor.Language
{
public abstract class RazorConfiguration
public abstract class RazorConfiguration : IEquatable<RazorConfiguration>
{
public static readonly RazorConfiguration Default = new DefaultRazorConfiguration(
RazorLanguageVersion.Latest,
@ -43,6 +44,58 @@ namespace Microsoft.AspNetCore.Razor.Language
public abstract RazorLanguageVersion LanguageVersion { get; }
public override bool Equals(object obj)
{
return base.Equals(obj as RazorConfiguration);
}
public virtual bool Equals(RazorConfiguration other)
{
if (object.ReferenceEquals(other, null))
{
return false;
}
if (LanguageVersion != other.LanguageVersion)
{
return false;
}
if (ConfigurationName != other.ConfigurationName)
{
return false;
}
if (Extensions.Count != other.Extensions.Count)
{
return false;
}
for (var i = 0; i < Extensions.Count; i++)
{
if (Extensions[i].ExtensionName != other.Extensions[i].ExtensionName)
{
return false;
}
}
return true;
}
public override int GetHashCode()
{
var hash = new HashCodeCombiner();
hash.Add(LanguageVersion);
hash.Add(ConfigurationName);
for (var i = 0; i < Extensions.Count; i++)
{
hash.Add(Extensions[i].ExtensionName);
}
return hash;
}
private class DefaultRazorConfiguration : RazorConfiguration
{
public DefaultRazorConfiguration(

View File

@ -0,0 +1,102 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
namespace Microsoft.CodeAnalysis.Razor
{
internal class DefaultProjectSnapshotProjectEngineFactory : ProjectSnapshotProjectEngineFactory
{
private readonly static RazorConfiguration DefaultConfiguration = FallbackRazorConfiguration.MVC_2_1;
private readonly IFallbackProjectEngineFactory _fallback;
private readonly Lazy<IProjectEngineFactory, ICustomProjectEngineFactoryMetadata>[] _factories;
public DefaultProjectSnapshotProjectEngineFactory(
IFallbackProjectEngineFactory fallback,
Lazy<IProjectEngineFactory, ICustomProjectEngineFactoryMetadata>[] factories)
{
if (fallback == null)
{
throw new ArgumentNullException(nameof(fallback));
}
if (factories == null)
{
throw new ArgumentNullException(nameof(factories));
}
_fallback = fallback;
_factories = factories;
}
public override RazorProjectEngine Create(ProjectSnapshot project, RazorProjectFileSystem fileSystem, Action<RazorProjectEngineBuilder> configure)
{
if (project == null)
{
throw new ArgumentNullException(nameof(project));
}
if (fileSystem == null)
{
throw new ArgumentNullException(nameof(fileSystem));
}
// When we're running in the editor, the editor provides a configure delegate that will include
// the editor settings and tag helpers.
//
// This service is only used in process in Visual Studio, and any other callers should provide these
// things also.
configure = configure ?? ((b) => { });
// The default configuration currently matches the newest MVC configuration.
//
// We typically want this because the language adds features over time - we don't want to a bunch of errors
// to show up when a document is first opened, and then go away when the configuration loads, we'd prefer the opposite.
var configuration = project.Configuration ?? DefaultConfiguration;
// If there's no factory to handle the configuration then fall back to a very basic configuration.
//
// This will stop a crash from happening in this case (misconfigured project), but will still make
// it obvious to the user that something is wrong.
var factory = SelectFactory(configuration) ?? _fallback;
return factory.Create(configuration, fileSystem, configure);
}
public override IProjectEngineFactory FindFactory(ProjectSnapshot project)
{
if (project == null)
{
throw new ArgumentNullException(nameof(project));
}
return SelectFactory(project.Configuration ?? DefaultConfiguration, requireSerializable: false);
}
public override IProjectEngineFactory FindSerializableFactory(ProjectSnapshot project)
{
if (project == null)
{
throw new ArgumentNullException(nameof(project));
}
return SelectFactory(project.Configuration ?? DefaultConfiguration, requireSerializable: true);
}
private IProjectEngineFactory SelectFactory(RazorConfiguration configuration, bool requireSerializable = false)
{
for (var i = 0; i < _factories.Length; i++)
{
var factory = _factories[i];
if (string.Equals(configuration.ConfigurationName, factory.Metadata.ConfigurationName))
{
return requireSerializable && !factory.Metadata.SupportsSerialization ? null : factory.Value;
}
}
return null;
}
}
}

View File

@ -0,0 +1,46 @@
// 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.Composition;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Host.Mef;
namespace Microsoft.CodeAnalysis.Razor.Workspaces
{
[ExportWorkspaceServiceFactory(typeof(ProjectSnapshotProjectEngineFactory))]
internal class DefaultProjectSnapshotProjectEngineFactoryFactory : IWorkspaceServiceFactory
{
private readonly IFallbackProjectEngineFactory _fallback;
private readonly Lazy<IProjectEngineFactory, ICustomProjectEngineFactoryMetadata>[] _factories;
[ImportingConstructor]
public DefaultProjectSnapshotProjectEngineFactoryFactory(
IFallbackProjectEngineFactory fallback,
[ImportMany] Lazy<IProjectEngineFactory, ICustomProjectEngineFactoryMetadata>[] factories)
{
if (fallback == null)
{
throw new ArgumentNullException(nameof(fallback));
}
if (factories == null)
{
throw new ArgumentNullException(nameof(factories));
}
_fallback = fallback;
_factories = factories;
}
public IWorkspaceService CreateService(HostWorkspaceServices workspaceServices)
{
if (workspaceServices == null)
{
throw new ArgumentNullException(nameof(workspaceServices));
}
return new DefaultProjectSnapshotProjectEngineFactory(_fallback, _factories);
}
}
}

View File

@ -0,0 +1,278 @@
// 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.Composition;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
using Microsoft.Extensions.Internal;
namespace Microsoft.CodeAnalysis.Razor
{
// Deliberately not exported for now, until this feature is working end to end.
internal class BackgroundDocumentGenerator : ProjectSnapshotChangeTrigger
{
private ForegroundDispatcher _foregroundDispatcher;
private ProjectSnapshotManagerBase _projectManager;
private readonly Dictionary<Key, DocumentSnapshot> _files;
private Timer _timer;
[ImportingConstructor]
public BackgroundDocumentGenerator(ForegroundDispatcher foregroundDispatcher)
{
if (foregroundDispatcher == null)
{
throw new ArgumentNullException(nameof(foregroundDispatcher));
}
_foregroundDispatcher = foregroundDispatcher;
_files = new Dictionary<Key, DocumentSnapshot>();
}
public bool HasPendingNotifications
{
get
{
lock (_files)
{
return _files.Count > 0;
}
}
}
// Used in unit tests to control the timer delay.
public TimeSpan Delay { get; set; } = TimeSpan.FromSeconds(2);
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 NotifyBackgroundWorkStarting { get; set; }
// Used in unit tests to ensure we can control when background work completes.
public ManualResetEventSlim BlockBackgroundWorkCompleting { get; set; }
// Used in unit tests to ensure we can know when background work finishes.
public ManualResetEventSlim NotifyBackgroundWorkCompleted { get; set; }
private void OnStartingBackgroundWork()
{
if (BlockBackgroundWorkStart != null)
{
BlockBackgroundWorkStart.Wait();
BlockBackgroundWorkStart.Reset();
}
if (NotifyBackgroundWorkStarting != null)
{
NotifyBackgroundWorkStarting.Set();
}
}
private void OnCompletingBackgroundWork()
{
if (BlockBackgroundWorkCompleting != null)
{
BlockBackgroundWorkCompleting.Wait();
BlockBackgroundWorkCompleting.Reset();
}
}
private void OnCompletedBackgroundWork()
{
if (NotifyBackgroundWorkCompleted != null)
{
NotifyBackgroundWorkCompleted.Set();
}
}
public override void Initialize(ProjectSnapshotManagerBase projectManager)
{
if (projectManager == null)
{
throw new ArgumentNullException(nameof(projectManager));
}
_projectManager = projectManager;
_projectManager.Changed += ProjectManager_Changed;
}
protected virtual Task ProcessDocument(DocumentSnapshot document)
{
return document.GetGeneratedOutputAsync();
}
public void Enqueue(ProjectSnapshot project, DocumentSnapshot document)
{
if (project == null)
{
throw new ArgumentNullException(nameof(project));
}
if (document == null)
{
throw new ArgumentNullException(nameof(document));
}
_foregroundDispatcher.AssertForegroundThread();
lock (_files)
{
// We only want to store the last 'seen' version of any given document. That way when we pick one to process
// it's always the best version to use.
_files.Add(new Key(project.FilePath, document.FilePath), document);
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();
DocumentSnapshot[] work;
lock (_files)
{
work = _files.Values.ToArray();
_files.Clear();
}
for (var i = 0; i < work.Length; i++)
{
var document = work[i];
try
{
await ProcessDocument(document);
}
catch (Exception ex)
{
ReportError(document, ex);
}
}
OnCompletingBackgroundWork();
lock (_files)
{
// 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 (_files.Count > 0)
{
StartWorker();
}
}
OnCompletedBackgroundWork();
}
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 ReportError(DocumentSnapshot document, Exception ex)
{
GC.KeepAlive(Task.Factory.StartNew(
() => _projectManager.ReportError(ex),
CancellationToken.None,
TaskCreationOptions.None,
_foregroundDispatcher.ForegroundScheduler));
}
private void ProjectManager_Changed(object sender, ProjectChangeEventArgs e)
{
switch (e.Kind)
{
case ProjectChangeKind.ProjectAdded:
case ProjectChangeKind.ProjectChanged:
case ProjectChangeKind.DocumentsChanged:
{
var project = _projectManager.GetLoadedProject(e.ProjectFilePath);
foreach (var documentFilePath in project.DocumentFilePaths)
{
Enqueue(project, project.GetDocument(documentFilePath));
}
break;
}
case ProjectChangeKind.DocumentContentChanged:
{
throw null;
}
case ProjectChangeKind.ProjectRemoved:
// ignore
break;
default:
throw new InvalidOperationException($"Unknown ProjectChangeKind {e.Kind}");
}
}
private struct Key : IEquatable<Key>
{
public Key(string projectFilePath, string documentFilePath)
{
ProjectFilePath = projectFilePath;
DocumentFilePath = documentFilePath;
}
public string ProjectFilePath { get; }
public string DocumentFilePath { get; }
public bool Equals(Key other)
{
return
FilePathComparer.Instance.Equals(ProjectFilePath, other.ProjectFilePath) &&
FilePathComparer.Instance.Equals(DocumentFilePath, other.DocumentFilePath);
}
public override bool Equals(object obj)
{
return obj is Key key ? Equals(key) : false;
}
public override int GetHashCode()
{
var hash = new HashCodeCombiner();
hash.Add(ProjectFilePath, FilePathComparer.Instance);
hash.Add(DocumentFilePath, FilePathComparer.Instance);
return hash;
}
}
}
}

View File

@ -0,0 +1,51 @@
// 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.IO;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
namespace Microsoft.CodeAnalysis.Razor
{
internal abstract class ProjectSnapshotProjectEngineFactory : IWorkspaceService
{
public abstract IProjectEngineFactory FindFactory(ProjectSnapshot project);
public abstract IProjectEngineFactory FindSerializableFactory(ProjectSnapshot project);
public RazorProjectEngine Create(ProjectSnapshot project)
{
return Create(project, RazorProjectFileSystem.Create(Path.GetDirectoryName(project.FilePath)), null);
}
public RazorProjectEngine Create(ProjectSnapshot project, RazorProjectFileSystem fileSystem)
{
if (project == null)
{
throw new ArgumentNullException(nameof(project));
}
if (fileSystem == null)
{
throw new ArgumentNullException(nameof(fileSystem));
}
return Create(project, fileSystem, null);
}
public RazorProjectEngine Create(ProjectSnapshot project, Action<RazorProjectEngineBuilder> configure)
{
if (project == null)
{
throw new ArgumentNullException(nameof(project));
}
return Create(project, RazorProjectFileSystem.Create(Path.GetDirectoryName(project.FilePath)), configure);
}
public abstract RazorProjectEngine Create(ProjectSnapshot project, RazorProjectFileSystem fileSystem, Action<RazorProjectEngineBuilder> configure);
}
}

View File

@ -0,0 +1,54 @@
// 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.Tasks;
using Microsoft.AspNetCore.Razor.Language;
namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
{
internal class DefaultDocumentSnapshot : DocumentSnapshot
{
public DefaultDocumentSnapshot(ProjectSnapshot project, DocumentState state)
{
if (project == null)
{
throw new ArgumentNullException(nameof(project));
}
if (state == null)
{
throw new ArgumentNullException(nameof(state));
}
Project = project;
State = state;
}
public ProjectSnapshot Project { get; }
public DocumentState State { get; }
public override string FilePath => State.HostDocument.FilePath;
public override string TargetPath => State.HostDocument.TargetPath;
public override Task<RazorCodeDocument> GetGeneratedOutputAsync()
{
// IMPORTANT: Don't put more code here. We want this to return a cached task.
return State.GeneratedOutput.GetGeneratedOutputInitializationTask(Project, this);
}
public override bool TryGetGeneratedOutput(out RazorCodeDocument results)
{
if (State.GeneratedOutput.IsResultAvailable)
{
results = State.GeneratedOutput.GetGeneratedOutputInitializationTask(Project, this).Result;
return true;
}
results = null;
return false;
}
}
}

View File

@ -3,182 +3,82 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.Language;
namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
{
// All of the public state of this is immutable - we create a new instance and notify subscribers
// when it changes.
//
// However we use the private state to track things like dirty/clean.
//
// See the private constructors... When we update the snapshot we either are processing a Workspace
// change (Project) or updating the computed state (ProjectSnapshotUpdateContext). We don't do both
// at once.
internal class DefaultProjectSnapshot : ProjectSnapshot
{
public DefaultProjectSnapshot(HostProject hostProject, Project workspaceProject, VersionStamp? version = null)
private readonly object _lock;
private Dictionary<string, DefaultDocumentSnapshot> _documents;
public DefaultProjectSnapshot(ProjectState state)
{
if (hostProject == null)
if (state == null)
{
throw new ArgumentNullException(nameof(hostProject));
throw new ArgumentNullException(nameof(state));
}
HostProject = hostProject;
WorkspaceProject = workspaceProject; // Might be null
FilePath = hostProject.FilePath;
Version = version ?? VersionStamp.Default;
State = state;
_lock = new object();
_documents = new Dictionary<string, DefaultDocumentSnapshot>(FilePathComparer.Instance);
}
private DefaultProjectSnapshot(HostProject hostProject, DefaultProjectSnapshot other)
{
if (hostProject == null)
{
throw new ArgumentNullException(nameof(hostProject));
}
if (other == null)
{
throw new ArgumentNullException(nameof(other));
}
ComputedVersion = other.ComputedVersion;
FilePath = other.FilePath;
TagHelpers = other.TagHelpers;
HostProject = hostProject;
WorkspaceProject = other.WorkspaceProject;
Version = other.Version.GetNewerVersion();
}
private DefaultProjectSnapshot(Project workspaceProject, DefaultProjectSnapshot other)
{
if (workspaceProject == null)
{
throw new ArgumentNullException(nameof(workspaceProject));
}
if (other == null)
{
throw new ArgumentNullException(nameof(other));
}
ComputedVersion = other.ComputedVersion;
FilePath = other.FilePath;
TagHelpers = other.TagHelpers;
HostProject = other.HostProject;
WorkspaceProject = workspaceProject;
Version = other.Version.GetNewerVersion();
}
private DefaultProjectSnapshot(ProjectSnapshotUpdateContext update, DefaultProjectSnapshot other)
{
if (update == null)
{
throw new ArgumentNullException(nameof(update));
}
if (other == null)
{
throw new ArgumentNullException(nameof(other));
}
ComputedVersion = update.Version;
FilePath = other.FilePath;
HostProject = other.HostProject;
TagHelpers = update.TagHelpers ?? Array.Empty<TagHelperDescriptor>();
WorkspaceProject = other.WorkspaceProject;
// This doesn't represent a new version of the underlying data. Keep the same version.
Version = other.Version;
}
public ProjectState State { get; }
public override RazorConfiguration Configuration => HostProject.Configuration;
public override string FilePath { get; }
public override IEnumerable<string> DocumentFilePaths => State.Documents.Keys;
public override HostProject HostProject { get; }
public override string FilePath => State.HostProject.FilePath;
public HostProject HostProject => State.HostProject;
public override bool IsInitialized => WorkspaceProject != null;
public override VersionStamp Version { get; }
public override VersionStamp Version => State.Version;
public override Project WorkspaceProject { get; }
public override Project WorkspaceProject => State.WorkspaceProject;
public override IReadOnlyList<TagHelperDescriptor> TagHelpers { get; } = Array.Empty<TagHelperDescriptor>();
// This is the version that the computed state is based on.
public VersionStamp? ComputedVersion { get; set; }
// We know the project is dirty if we don't have a computed result, or it was computed for a different version.
// Since the PSM updates the snapshots synchronously, the snapshot can never be older than the computed state.
public bool IsDirty => ComputedVersion == null || ComputedVersion.Value != Version;
public ProjectSnapshotUpdateContext CreateUpdateContext()
public override DocumentSnapshot GetDocument(string filePath)
{
return new ProjectSnapshotUpdateContext(FilePath, HostProject, WorkspaceProject, Version);
lock (_lock)
{
if (!_documents.TryGetValue(filePath, out var result) &&
State.Documents.TryGetValue(filePath, out var state))
{
result = new DefaultDocumentSnapshot(this, state);
_documents.Add(filePath, result);
}
return result;
}
}
public DefaultProjectSnapshot WithHostProject(HostProject hostProject)
public override RazorProjectEngine GetProjectEngine()
{
if (hostProject == null)
return State.ProjectEngine.GetProjectEngine(this);
}
public override Task<IReadOnlyList<TagHelperDescriptor>> GetTagHelpersAsync()
{
// IMPORTANT: Don't put more code here. We want this to return a cached task.
return State.TagHelpers.GetTagHelperInitializationTask(this);
}
public override bool TryGetTagHelpers(out IReadOnlyList<TagHelperDescriptor> results)
{
if (State.TagHelpers.IsResultAvailable)
{
throw new ArgumentNullException(nameof(hostProject));
results = State.TagHelpers.GetTagHelperInitializationTask(this).Result;
return true;
}
return new DefaultProjectSnapshot(hostProject, this);
}
public DefaultProjectSnapshot RemoveWorkspaceProject()
{
// We want to get rid of all of the computed state since it's not really valid.
return new DefaultProjectSnapshot(HostProject, null, Version.GetNewerVersion());
}
public DefaultProjectSnapshot WithWorkspaceProject(Project workspaceProject)
{
if (workspaceProject == null)
{
throw new ArgumentNullException(nameof(workspaceProject));
}
return new DefaultProjectSnapshot(workspaceProject, this);
}
public DefaultProjectSnapshot WithComputedUpdate(ProjectSnapshotUpdateContext update)
{
if (update == null)
{
throw new ArgumentNullException(nameof(update));
}
return new DefaultProjectSnapshot(update, this);
}
public bool HasConfigurationChanged(DefaultProjectSnapshot original)
{
if (original == null)
{
throw new ArgumentNullException(nameof(original));
}
return !object.Equals(Configuration, original.Configuration);
}
public bool HaveTagHelpersChanged(ProjectSnapshot original)
{
if (original == null)
{
throw new ArgumentNullException(nameof(original));
}
return !Enumerable.SequenceEqual(TagHelpers, original.TagHelpers);
results = null;
return false;
}
}
}

View File

@ -3,7 +3,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
@ -31,15 +30,14 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
private readonly ErrorReporter _errorReporter;
private readonly ForegroundDispatcher _foregroundDispatcher;
private readonly ProjectSnapshotChangeTrigger[] _triggers;
private readonly ProjectSnapshotWorkerQueue _workerQueue;
private readonly ProjectSnapshotWorker _worker;
private readonly Dictionary<string, DefaultProjectSnapshot> _projects;
// Each entry holds a ProjectState and an optional ProjectSnapshot. ProjectSnapshots are
// created lazily.
private readonly Dictionary<string, Entry> _projects;
public DefaultProjectSnapshotManager(
ForegroundDispatcher foregroundDispatcher,
ErrorReporter errorReporter,
ProjectSnapshotWorker worker,
IEnumerable<ProjectSnapshotChangeTrigger> triggers,
Workspace workspace)
{
@ -53,11 +51,6 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
throw new ArgumentNullException(nameof(errorReporter));
}
if (worker == null)
{
throw new ArgumentNullException(nameof(worker));
}
if (triggers == null)
{
throw new ArgumentNullException(nameof(triggers));
@ -70,13 +63,10 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
_foregroundDispatcher = foregroundDispatcher;
_errorReporter = errorReporter;
_worker = worker;
_triggers = triggers.ToArray();
Workspace = workspace;
_projects = new Dictionary<string, DefaultProjectSnapshot>(FilePathComparer.Instance);
_workerQueue = new ProjectSnapshotWorkerQueue(_foregroundDispatcher, this, worker);
_projects = new Dictionary<string, Entry>(FilePathComparer.Instance);
for (var i = 0; i < _triggers.Length; i++)
{
@ -89,43 +79,109 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
get
{
_foregroundDispatcher.AssertForegroundThread();
return _projects.Values.ToArray();
var i = 0;
var projects = new ProjectSnapshot[_projects.Count];
foreach (var entry in _projects)
{
if (entry.Value.Snapshot == null)
{
entry.Value.Snapshot = new DefaultProjectSnapshot(entry.Value.State);
}
projects[i++] = entry.Value.Snapshot;
}
return projects;
}
}
public override Workspace Workspace { get; }
public override void ProjectUpdated(ProjectSnapshotUpdateContext update)
public override ProjectSnapshot GetLoadedProject(string filePath)
{
if (update == null)
if (filePath == null)
{
throw new ArgumentNullException(nameof(update));
throw new ArgumentNullException(nameof(filePath));
}
_foregroundDispatcher.AssertForegroundThread();
if (_projects.TryGetValue(update.WorkspaceProject.FilePath, out var original))
if (_projects.TryGetValue(filePath, out var entry))
{
if (!original.IsInitialized)
if (entry.Snapshot == null)
{
// If the project has been uninitialized, just ignore the update.
return;
entry.Snapshot = new DefaultProjectSnapshot(entry.State);
}
// This is an update to the project's computed values, so everything should be overwritten
var snapshot = original.WithComputedUpdate(update);
_projects[update.WorkspaceProject.FilePath] = snapshot;
return entry.Snapshot;
}
if (snapshot.IsDirty)
return null;
}
public override ProjectSnapshot GetOrCreateProject(string filePath)
{
if (filePath == null)
{
throw new ArgumentNullException(nameof(filePath));
}
_foregroundDispatcher.AssertForegroundThread();
return GetLoadedProject(filePath) ?? new EphemeralProjectSnapshot(Workspace.Services, filePath);
}
public override void DocumentAdded(HostProject hostProject, HostDocument document)
{
if (hostProject == null)
{
throw new ArgumentNullException(nameof(hostProject));
}
if (document == null)
{
throw new ArgumentNullException(nameof(document));
}
_foregroundDispatcher.AssertForegroundThread();
if (_projects.TryGetValue(hostProject.FilePath, out var entry))
{
var state = entry.State.AddHostDocument(document);
// Document updates can no-op.
if (!object.ReferenceEquals(state, entry.State))
{
// 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(snapshot.CreateUpdateContext());
_projects[hostProject.FilePath] = new Entry(state);
NotifyListeners(new ProjectChangeEventArgs(hostProject.FilePath, ProjectChangeKind.DocumentsChanged));
}
if (!object.Equals(snapshot.ComputedVersion, original.ComputedVersion))
}
}
public override void DocumentRemoved(HostProject hostProject, HostDocument document)
{
if (hostProject == null)
{
throw new ArgumentNullException(nameof(hostProject));
}
if (document == null)
{
throw new ArgumentNullException(nameof(document));
}
_foregroundDispatcher.AssertForegroundThread();
if (_projects.TryGetValue(hostProject.FilePath, out var entry))
{
var state = entry.State.RemoveHostDocument(document);
// Document updates can no-op.
if (!object.ReferenceEquals(state, entry.State))
{
NotifyListeners(new ProjectChangeEventArgs(snapshot, ProjectChangeKind.TagHelpersChanged));
_projects[hostProject.FilePath] = new Entry(state);
NotifyListeners(new ProjectChangeEventArgs(hostProject.FilePath, ProjectChangeKind.DocumentsChanged));
}
}
}
@ -149,17 +205,11 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
// So if possible find a WorkspaceProject.
var workspaceProject = GetWorkspaceProject(hostProject.FilePath);
var snapshot = new DefaultProjectSnapshot(hostProject, workspaceProject);
_projects[hostProject.FilePath] = snapshot;
if (snapshot.IsInitialized && snapshot.IsDirty)
{
// Start computing background state if the project is fully initialized.
NotifyBackgroundWorker(snapshot.CreateUpdateContext());
}
var state = new ProjectState(Workspace.Services, hostProject, workspaceProject);
_projects[hostProject.FilePath] = new Entry(state);
// We need to notify listeners about every project add.
NotifyListeners(new ProjectChangeEventArgs(snapshot, ProjectChangeKind.Added));
NotifyListeners(new ProjectChangeEventArgs(hostProject.FilePath, ProjectChangeKind.ProjectAdded));
}
public override void HostProjectChanged(HostProject hostProject)
@ -171,22 +221,17 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
_foregroundDispatcher.AssertForegroundThread();
if (_projects.TryGetValue(hostProject.FilePath, out var original))
if (_projects.TryGetValue(hostProject.FilePath, out var entry))
{
// Doing an update to the project should keep computed values, but mark the project as dirty if the
// underlying project is newer.
var snapshot = original.WithHostProject(hostProject);
_projects[hostProject.FilePath] = snapshot;
var state = entry.State.WithHostProject(hostProject);
if (snapshot.IsInitialized && snapshot.IsDirty)
// HostProject updates can no-op.
if (!object.ReferenceEquals(state, entry.State))
{
// Start computing background state if the project is fully initialized.
NotifyBackgroundWorker(snapshot.CreateUpdateContext());
}
_projects[hostProject.FilePath] = new Entry(state);
// Notify listeners right away because if the HostProject changes then it's likely that the Razor
// configuration changed.
NotifyListeners(new ProjectChangeEventArgs(snapshot, ProjectChangeKind.Changed));
NotifyListeners(new ProjectChangeEventArgs(hostProject.FilePath, ProjectChangeKind.ProjectChanged));
}
}
}
@ -204,37 +249,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
_projects.Remove(hostProject.FilePath);
// We need to notify listeners about every project removal.
NotifyListeners(new ProjectChangeEventArgs(snapshot, ProjectChangeKind.Removed));
}
}
public override void HostProjectBuildComplete(HostProject hostProject)
{
if (hostProject == null)
{
throw new ArgumentNullException(nameof(hostProject));
}
_foregroundDispatcher.AssertForegroundThread();
if (_projects.TryGetValue(hostProject.FilePath, out var original))
{
var workspaceProject = GetWorkspaceProject(hostProject.FilePath);
if (workspaceProject == null)
{
// Host project was built prior to a workspace project being associated. We have nothing to do without
// a workspace project so we short circuit.
return;
}
// Doing an update to the project should keep computed values, but mark the project as dirty if the
// underlying project is newer.
var snapshot = original.WithWorkspaceProject(workspaceProject);
_projects[hostProject.FilePath] = snapshot;
// Notify the background worker so it can trigger tag helper discovery.
NotifyBackgroundWorker(snapshot.CreateUpdateContext());
NotifyListeners(new ProjectChangeEventArgs(hostProject.FilePath, ProjectChangeKind.ProjectRemoved));
}
}
@ -254,25 +269,16 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
// The WorkspaceProject initialization never triggers a "Project Add" from out point of view, we
// only care if the new WorkspaceProject matches an existing HostProject.
if (_projects.TryGetValue(workspaceProject.FilePath, out var original))
if (_projects.TryGetValue(workspaceProject.FilePath, out var entry))
{
// If this is a multi-targeting project then we are only interested in a single workspace project. If we already
// found one in the past just ignore this one.
if (original.WorkspaceProject == null)
if (entry.State.WorkspaceProject == null)
{
var snapshot = original.WithWorkspaceProject(workspaceProject);
_projects[workspaceProject.FilePath] = snapshot;
var state = entry.State.WithWorkspaceProject(workspaceProject);
_projects[workspaceProject.FilePath] = new Entry(state);
if (snapshot.IsInitialized && snapshot.IsDirty)
{
// 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(snapshot.CreateUpdateContext());
}
// Notify listeners right away since WorkspaceProject was just added, the project is now initialized.
NotifyListeners(new ProjectChangeEventArgs(snapshot, ProjectChangeKind.Changed));
NotifyListeners(new ProjectChangeEventArgs(workspaceProject.FilePath, ProjectChangeKind.ProjectChanged));
}
}
}
@ -293,25 +299,18 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
// We also need to check the projectId here. If this is a multi-targeting project then we are only interested
// in a single workspace project. Just use the one that showed up first.
if (_projects.TryGetValue(workspaceProject.FilePath, out var original) &&
(original.WorkspaceProject == null ||
original.WorkspaceProject.Id == workspaceProject.Id))
if (_projects.TryGetValue(workspaceProject.FilePath, out var entry) &&
(entry.State.WorkspaceProject == null || entry.State.WorkspaceProject.Id == workspaceProject.Id))
{
// Doing an update to the project should keep computed values, but mark the project as dirty if the
// underlying project is newer.
var snapshot = original.WithWorkspaceProject(workspaceProject);
_projects[workspaceProject.FilePath] = snapshot;
if (snapshot.IsInitialized && snapshot.IsDirty)
var state = entry.State.WithWorkspaceProject(workspaceProject);
// WorkspaceProject updates can no-op. This can be the case if a build is triggered, but we've
// already seen the update.
if (!object.ReferenceEquals(state, entry.State))
{
// 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(snapshot.CreateUpdateContext());
}
_projects[workspaceProject.FilePath] = new Entry(state);
if (snapshot.HaveTagHelpersChanged(original))
{
NotifyListeners(new ProjectChangeEventArgs(snapshot, ProjectChangeKind.TagHelpersChanged));
NotifyListeners(new ProjectChangeEventArgs(workspaceProject.FilePath, ProjectChangeKind.ProjectChanged));
}
}
}
@ -330,16 +329,16 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
return;
}
if (_projects.TryGetValue(workspaceProject.FilePath, out var original))
if (_projects.TryGetValue(workspaceProject.FilePath, out var entry))
{
// We also need to check the projectId here. If this is a multi-targeting project then we are only interested
// in a single workspace project. Make sure the WorkspaceProject we're using is the one that's being removed.
if (original.WorkspaceProject?.Id != workspaceProject.Id)
if (entry.State.WorkspaceProject?.Id != workspaceProject.Id)
{
return;
}
DefaultProjectSnapshot snapshot;
ProjectState state;
// So if the WorkspaceProject got removed, we should double check to make sure that there aren't others
// hanging around. This could happen if a project is multi-targeting and one of the TFMs is removed.
@ -347,30 +346,19 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
if (otherWorkspaceProject != null && otherWorkspaceProject.Id != workspaceProject.Id)
{
// OK there's another WorkspaceProject, use that.
//
// Doing an update to the project should keep computed values, but mark the project as dirty if the
// underlying project is newer.
snapshot = original.WithWorkspaceProject(otherWorkspaceProject);
_projects[workspaceProject.FilePath] = snapshot;
state = entry.State.WithWorkspaceProject(otherWorkspaceProject);
_projects[otherWorkspaceProject.FilePath] = new Entry(state);
if (snapshot.IsInitialized && snapshot.IsDirty)
{
// 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(snapshot.CreateUpdateContext());
}
// Notify listeners of a change because it's a different WorkspaceProject.
NotifyListeners(new ProjectChangeEventArgs(snapshot, ProjectChangeKind.Changed));
return;
NotifyListeners(new ProjectChangeEventArgs(otherWorkspaceProject.FilePath, ProjectChangeKind.ProjectChanged));
}
else
{
state = entry.State.WithWorkspaceProject(null);
_projects[workspaceProject.FilePath] = new Entry(state);
snapshot = original.RemoveWorkspaceProject();
_projects[workspaceProject.FilePath] = snapshot;
// Notify listeners of a change because we've removed computed state.
NotifyListeners(new ProjectChangeEventArgs(snapshot, ProjectChangeKind.Changed));
// Notify listeners of a change because we've removed computed state.
NotifyListeners(new ProjectChangeEventArgs(workspaceProject.FilePath, ProjectChangeKind.ProjectChanged));
}
}
}
@ -401,8 +389,8 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
throw new ArgumentNullException(nameof(exception));
}
var project = hostProject?.FilePath == null ? null : this.GetProjectWithFilePath(hostProject.FilePath);
_errorReporter.ReportError(exception, project);
var snapshot = hostProject?.FilePath == null ? null : GetLoadedProject(hostProject.FilePath);
_errorReporter.ReportError(exception, snapshot);
}
public override void ReportError(Exception exception, Project workspaceProject)
@ -411,7 +399,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
{
throw new ArgumentNullException(nameof(exception));
}
_errorReporter.ReportError(exception, workspaceProject);
}
@ -440,14 +428,6 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
return null;
}
// virtual so it can be overridden in tests
protected virtual void NotifyBackgroundWorker(ProjectSnapshotUpdateContext context)
{
_foregroundDispatcher.AssertForegroundThread();
_workerQueue.Enqueue(context);
}
// virtual so it can be overridden in tests
protected virtual void NotifyListeners(ProjectChangeEventArgs e)
{
@ -459,5 +439,16 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
handler(this, e);
}
}
private class Entry
{
public ProjectSnapshot Snapshot;
public readonly ProjectState State;
public Entry(ProjectState state)
{
State = state;
}
}
}
}

View File

@ -45,8 +45,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
return new DefaultProjectSnapshotManager(
_foregroundDispatcher,
languageServices.WorkspaceServices.GetRequiredService<ErrorReporter>(),
languageServices.GetRequiredService<ProjectSnapshotWorker>(),
_triggers,
_triggers,
languageServices.WorkspaceServices.Workspace);
}
}

View File

@ -1,62 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Threading;
using System.Threading.Tasks;
namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
{
internal class DefaultProjectSnapshotWorker : ProjectSnapshotWorker
{
private readonly ForegroundDispatcher _foregroundDispatcher;
private readonly TagHelperResolver _tagHelperResolver;
public DefaultProjectSnapshotWorker(ForegroundDispatcher foregroundDispatcher, TagHelperResolver tagHelperResolver)
{
if (foregroundDispatcher == null)
{
throw new ArgumentNullException(nameof(foregroundDispatcher));
}
if (tagHelperResolver == null)
{
throw new ArgumentNullException(nameof(tagHelperResolver));
}
_foregroundDispatcher = foregroundDispatcher;
_tagHelperResolver = tagHelperResolver;
}
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, TaskCreationOptions.None, _foregroundDispatcher.BackgroundScheduler);
}
return ProjectUpdatesCoreAsync(update);
}
protected virtual void OnProcessingUpdate()
{
}
private async Task ProjectUpdatesCoreAsync(object state)
{
var update = (ProjectSnapshotUpdateContext)state;
OnProcessingUpdate();
var snapshot = new DefaultProjectSnapshot(update.HostProject, update.WorkspaceProject, update.Version);
var result = await _tagHelperResolver.GetTagHelpersAsync(snapshot, CancellationToken.None);
update.TagHelpers = result.Descriptors;
}
}
}

View File

@ -1,32 +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.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
{
private readonly ForegroundDispatcher _foregroundDispatcher;
[ImportingConstructor]
public DefaultProjectSnapshotWorkerFactory(ForegroundDispatcher foregroundDispatcher)
{
if (foregroundDispatcher == null)
{
throw new System.ArgumentNullException(nameof(foregroundDispatcher));
}
_foregroundDispatcher = foregroundDispatcher;
}
public ILanguageService CreateLanguageService(HostLanguageServices languageServices)
{
return new DefaultProjectSnapshotWorker(_foregroundDispatcher, languageServices.GetRequiredService<TagHelperResolver>());
}
}
}

View File

@ -0,0 +1,111 @@
// 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.Threading.Tasks;
using Microsoft.AspNetCore.Razor.Language;
namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
{
internal class DocumentGeneratedOutputTracker
{
// Don't keep anything if the configuration has changed. It's OK if the
// workspace project has changes, we'll consider whether the tag helpers are
// difference before reusing a previous result.
private const ProjectDifference Mask = ProjectDifference.ConfigurationChanged;
private readonly object _lock;
private DocumentGeneratedOutputTracker _older;
private Task<RazorCodeDocument> _task;
private IReadOnlyList<TagHelperDescriptor> _tagHelpers;
public DocumentGeneratedOutputTracker(DocumentGeneratedOutputTracker older)
{
_older = older;
_lock = new object();
}
public bool IsResultAvailable => _task?.IsCompleted == true;
public Task<RazorCodeDocument> GetGeneratedOutputInitializationTask(ProjectSnapshot project, DocumentSnapshot document)
{
if (project == null)
{
throw new ArgumentNullException(nameof(project));
}
if (document == null)
{
throw new ArgumentNullException(nameof(document));
}
if (_task == null)
{
lock (_lock)
{
if (_task == null)
{
_task = GetGeneratedOutputInitializationTaskCore(project, document);
}
}
}
return _task;
}
public DocumentGeneratedOutputTracker ForkFor(DocumentState state, ProjectDifference difference)
{
if (state == null)
{
throw new ArgumentNullException(nameof(state));
}
if ((difference & Mask) != 0)
{
return null;
}
return new DocumentGeneratedOutputTracker(this);
}
private async Task<RazorCodeDocument> GetGeneratedOutputInitializationTaskCore(ProjectSnapshot project, DocumentSnapshot document)
{
var tagHelpers = await project.GetTagHelpersAsync().ConfigureAwait(false);
if (_older != null && _older.IsResultAvailable)
{
var difference = new HashSet<TagHelperDescriptor>(TagHelperDescriptorComparer.Default);
difference.UnionWith(_older._tagHelpers);
difference.SymmetricExceptWith(tagHelpers);
if (difference.Count == 0)
{
// We can use the cached result.
var result = _older._task.Result;
// Drop reference so it can be GC'ed
_older = null;
// Cache the tag helpers so the next version can use them
_tagHelpers = tagHelpers;
return result;
}
}
// Drop reference so it can be GC'ed
_older = null;
// Cache the tag helpers so the next version can use them
_tagHelpers = tagHelpers;
var projectEngine = project.GetProjectEngine();
var projectItem = projectEngine.FileSystem.GetItem(document.FilePath);
return projectItem == null ? null : projectEngine.ProcessDesignTime(projectItem);
}
}
}

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.Threading.Tasks;
using Microsoft.AspNetCore.Razor.Language;
namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
{
internal abstract class DocumentSnapshot
{
public abstract string FilePath { get; }
public abstract string TargetPath { get; }
public abstract Task<RazorCodeDocument> GetGeneratedOutputAsync();
public abstract bool TryGetGeneratedOutput(out RazorCodeDocument results);
}
}

View File

@ -0,0 +1,73 @@
// 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.ProjectSystem
{
internal class DocumentState
{
private readonly object _lock;
private DocumentGeneratedOutputTracker _generatedOutput;
public DocumentState(HostWorkspaceServices services, HostDocument hostDocument)
{
if (services == null)
{
throw new ArgumentNullException(nameof(services));
}
if (hostDocument == null)
{
throw new ArgumentNullException(nameof(hostDocument));
}
Services = services;
HostDocument = hostDocument;
Version = VersionStamp.Create();
_lock = new object();
}
public DocumentState(DocumentState previous, ProjectDifference difference)
{
if (previous == null)
{
throw new ArgumentNullException(nameof(previous));
}
Services = previous.Services;
HostDocument = previous.HostDocument;
Version = previous.Version.GetNewerVersion();
_generatedOutput = previous._generatedOutput?.ForkFor(this, difference);
}
public HostDocument HostDocument { get; }
public HostWorkspaceServices Services { get; }
public VersionStamp Version { get; }
public DocumentGeneratedOutputTracker GeneratedOutput
{
get
{
if (_generatedOutput == null)
{
lock (_lock)
{
if (_generatedOutput == null)
{
_generatedOutput = new DocumentGeneratedOutputTracker(null);
}
}
}
return _generatedOutput;
}
}
}
}

View File

@ -0,0 +1,81 @@
// 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.Threading.Tasks;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.CodeAnalysis.Host;
namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
{
internal class EphemeralProjectSnapshot : ProjectSnapshot
{
private static readonly Task<IReadOnlyList<TagHelperDescriptor>> EmptyTagHelpers = Task.FromResult<IReadOnlyList<TagHelperDescriptor>>(Array.Empty<TagHelperDescriptor>());
private readonly HostWorkspaceServices _services;
private readonly Lazy<RazorProjectEngine> _projectEngine;
public EphemeralProjectSnapshot(HostWorkspaceServices services, string filePath)
{
if (services == null)
{
throw new ArgumentNullException(nameof(services));
}
if (filePath == null)
{
throw new ArgumentNullException(nameof(filePath));
}
_services = services;
FilePath = filePath;
_projectEngine = new Lazy<RazorProjectEngine>(CreateProjectEngine);
}
public override RazorConfiguration Configuration => FallbackRazorConfiguration.MVC_2_1;
public override IEnumerable<string> DocumentFilePaths => Array.Empty<string>();
public override string FilePath { get; }
public override bool IsInitialized => false;
public override VersionStamp Version { get; } = VersionStamp.Default;
public override Project WorkspaceProject => null;
public override DocumentSnapshot GetDocument(string filePath)
{
if (filePath == null)
{
throw new ArgumentNullException(nameof(filePath));
}
return null;
}
public override RazorProjectEngine GetProjectEngine()
{
return _projectEngine.Value;
}
public override Task<IReadOnlyList<TagHelperDescriptor>> GetTagHelpersAsync()
{
return EmptyTagHelpers;
}
public override bool TryGetTagHelpers(out IReadOnlyList<TagHelperDescriptor> results)
{
results = EmptyTagHelpers.Result;
return true;
}
private RazorProjectEngine CreateProjectEngine()
{
var factory = _services.GetRequiredService<ProjectSnapshotProjectEngineFactory>();
return factory.Create(this);
}
}
}

View File

@ -0,0 +1,61 @@
// 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.Extensions.Internal;
namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
{
internal class HostDocument : IEquatable<HostDocument>
{
public HostDocument(string filePath, string targetPath)
{
if (filePath == null)
{
throw new ArgumentNullException(nameof(filePath));
}
if (targetPath == null)
{
throw new ArgumentNullException(nameof(targetPath));
}
FilePath = filePath;
TargetPath = targetPath;
}
public string FilePath { get; }
public string TargetPath { get; }
public override bool Equals(object obj)
{
return base.Equals(obj as DocumentSnapshot);
}
public bool Equals(DocumentSnapshot other)
{
if (ReferenceEquals(null, other))
{
return false;
}
return
FilePathComparer.Instance.Equals(FilePath, other.FilePath) &&
FilePathComparer.Instance.Equals(TargetPath, other.TargetPath);
}
public bool Equals(HostDocument other)
{
throw new NotImplementedException();
}
public override int GetHashCode()
{
var hash = new HashCodeCombiner();
hash.Add(FilePath, FilePathComparer.Instance);
hash.Add(TargetPath, FilePathComparer.Instance);
return hash;
}
}
}

View File

@ -7,13 +7,13 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
{
internal class ProjectChangeEventArgs : EventArgs
{
public ProjectChangeEventArgs(ProjectSnapshot project, ProjectChangeKind kind)
public ProjectChangeEventArgs(string projectFilePath, ProjectChangeKind kind)
{
Project = project;
ProjectFilePath = projectFilePath;
Kind = kind;
}
public ProjectSnapshot Project { get; }
public string ProjectFilePath { get; }
public ProjectChangeKind Kind { get; }
}

View File

@ -5,9 +5,10 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
{
internal enum ProjectChangeKind
{
Added,
Removed,
Changed,
TagHelpersChanged,
ProjectAdded,
ProjectRemoved,
ProjectChanged,
DocumentsChanged,
DocumentContentChanged,
}
}

View File

@ -0,0 +1,18 @@
// 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
{
[Flags]
internal enum ProjectDifference
{
None = 0,
ConfigurationChanged = 1,
WorkspaceProjectAdded = 2,
WorkspaceProjectRemoved = 4,
WorkspaceProjectChanged = 8,
DocumentsChanged = 16,
}
}

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.AspNetCore.Razor.Language;
using Microsoft.CodeAnalysis.Host;
namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
{
internal class ProjectEngineTracker
{
private const ProjectDifference Mask = ProjectDifference.ConfigurationChanged;
private readonly object _lock = new object();
private readonly HostWorkspaceServices _services;
private RazorProjectEngine _projectEngine;
public ProjectEngineTracker(ProjectState state)
{
if (state == null)
{
throw new ArgumentNullException(nameof(state));
}
_services = state.Services;
}
public ProjectEngineTracker ForkFor(ProjectState state, ProjectDifference difference)
{
if (state == null)
{
throw new ArgumentNullException(nameof(state));
}
if ((difference & Mask) != 0)
{
return null;
}
return this;
}
public RazorProjectEngine GetProjectEngine(ProjectSnapshot snapshot)
{
if (snapshot == null)
{
throw new ArgumentNullException(nameof(snapshot));
}
if (_projectEngine == null)
{
lock (_lock)
{
if (_projectEngine == null)
{
var factory = _services.GetRequiredService<ProjectSnapshotProjectEngineFactory>();
_projectEngine = factory.Create(snapshot);
}
}
}
return _projectEngine;
}
}
}

View File

@ -1,42 +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 sealed class ProjectExtensibilityAssembly : IEquatable<ProjectExtensibilityAssembly>
{
public ProjectExtensibilityAssembly(AssemblyIdentity identity)
{
if (identity == null)
{
throw new ArgumentNullException(nameof(identity));
}
Identity = identity;
}
public AssemblyIdentity Identity { get; }
public bool Equals(ProjectExtensibilityAssembly other)
{
if (other == null)
{
return false;
}
return Identity.Equals(other.Identity);
}
public override int GetHashCode()
{
return Identity.GetHashCode();
}
public override bool Equals(object obj)
{
return base.Equals(obj as ProjectExtensibilityAssembly);
}
}
}

View File

@ -1,8 +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;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.Language;
namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
@ -11,16 +11,22 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
{
public abstract RazorConfiguration Configuration { get; }
public abstract IEnumerable<string> DocumentFilePaths { get; }
public abstract string FilePath { get; }
public abstract bool IsInitialized { get; }
public abstract IReadOnlyList<TagHelperDescriptor> TagHelpers { get; }
public abstract VersionStamp Version { get; }
public abstract Project WorkspaceProject { get; }
public abstract HostProject HostProject { get; }
public abstract RazorProjectEngine GetProjectEngine();
public abstract DocumentSnapshot GetDocument(string filePath);
public abstract Task<IReadOnlyList<TagHelperDescriptor>> GetTagHelpersAsync();
public abstract bool TryGetTagHelpers(out IReadOnlyList<TagHelperDescriptor> results);
}
}

View File

@ -12,5 +12,9 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
public abstract event EventHandler<ProjectChangeEventArgs> Changed;
public abstract IReadOnlyList<ProjectSnapshot> Projects { get; }
public abstract ProjectSnapshot GetLoadedProject(string filePath);
public abstract ProjectSnapshot GetOrCreateProject(string filePath);
}
}

View File

@ -9,7 +9,9 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
{
public abstract Workspace Workspace { get; }
public abstract void ProjectUpdated(ProjectSnapshotUpdateContext update);
public abstract void DocumentAdded(HostProject hostProject, HostDocument hostDocument);
public abstract void DocumentRemoved(HostProject hostProject, HostDocument hostDocument);
public abstract void HostProjectAdded(HostProject hostProject);
@ -17,8 +19,6 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
public abstract void HostProjectRemoved(HostProject hostProject);
public abstract void HostProjectBuildComplete(HostProject hostProject);
public abstract void WorkspaceProjectAdded(Project workspaceProject);
public abstract void WorkspaceProjectChanged(Project workspaceProject);

View File

@ -1,25 +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 static class ProjectSnapshotManagerExtensions
{
public static ProjectSnapshot GetProjectWithFilePath(this ProjectSnapshotManager snapshotManager, string filePath)
{
var projects = snapshotManager.Projects;
for (var i = 0; i< projects.Count; i++)
{
var project = projects[i];
if (FilePathComparer.Instance.Equals(filePath, project.FilePath))
{
return project;
}
}
return null;
}
}
}

View File

@ -1,45 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Razor.Language;
namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
{
internal class ProjectSnapshotUpdateContext
{
public ProjectSnapshotUpdateContext(string filePath, HostProject hostProject, Project workspaceProject, VersionStamp version)
{
if (filePath == null)
{
throw new ArgumentNullException(nameof(filePath));
}
if (hostProject == null)
{
throw new ArgumentNullException(nameof(hostProject));
}
if (workspaceProject == null)
{
throw new ArgumentNullException(nameof(workspaceProject));
}
FilePath = filePath;
HostProject = hostProject;
WorkspaceProject = workspaceProject;
Version = version;
}
public string FilePath { get; }
public HostProject HostProject { get; }
public Project WorkspaceProject { get; }
public IReadOnlyList<TagHelperDescriptor> TagHelpers { get; set; }
public VersionStamp Version { get; }
}
}

View File

@ -1,14 +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.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

@ -1,203 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
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<string, ProjectSnapshotUpdateContext> _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<string, ProjectSnapshotUpdateContext>(FilePathComparer.Instance);
}
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);
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; }
private void OnStartingBackgroundWork()
{
if (BlockBackgroundWorkStart != null)
{
BlockBackgroundWorkStart.Wait();
BlockBackgroundWorkStart.Reset();
}
}
private void OnFinishingBackgroundWork()
{
if (NotifyBackgroundWorkFinish != null)
{
NotifyBackgroundWorkFinish.Set();
}
}
private void OnFinishingForegroundWork()
{
if (NotifyForegroundWorkFinish != null)
{
NotifyForegroundWorkFinish.Set();
}
}
public void Enqueue(ProjectSnapshotUpdateContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
_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[context.FilePath] = context;
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();
ProjectSnapshotUpdateContext[] 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] = (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?.WorkspaceProject);
}
}
}
}
}

View File

@ -0,0 +1,239 @@
// 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;
namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
{
// Internal tracker for DefaultProjectSnapshot
internal class ProjectState
{
private static readonly IReadOnlyDictionary<string, DocumentState> EmptyDocuments = new Dictionary<string, DocumentState>();
private readonly object _lock;
private ProjectEngineTracker _projectEngine;
private ProjectTagHelperTracker _tagHelpers;
public ProjectState(
HostWorkspaceServices services,
HostProject hostProject,
Project workspaceProject)
{
if (services == null)
{
throw new ArgumentNullException(nameof(services));
}
if (hostProject == null)
{
throw new ArgumentNullException(nameof(hostProject));
}
Services = services;
HostProject = hostProject;
WorkspaceProject = workspaceProject;
Documents = EmptyDocuments;
Version = VersionStamp.Create();
_lock = new object();
}
public ProjectState(
ProjectState older,
ProjectDifference difference,
HostProject hostProject,
Project workspaceProject,
IReadOnlyDictionary<string, DocumentState> documents)
{
if (older == null)
{
throw new ArgumentNullException(nameof(older));
}
if (hostProject == null)
{
throw new ArgumentNullException(nameof(hostProject));
}
if (documents == null)
{
throw new ArgumentNullException(nameof(documents));
}
Services = older.Services;
Version = older.Version.GetNewerVersion();
HostProject = hostProject;
WorkspaceProject = workspaceProject;
Documents = documents;
_lock = new object();
_projectEngine = older._projectEngine?.ForkFor(this, difference);
_tagHelpers = older._tagHelpers?.ForkFor(this, difference);
}
public IReadOnlyDictionary<string, DocumentState> Documents { get; }
public HostProject HostProject { get; }
public HostWorkspaceServices Services { get; }
public Project WorkspaceProject { get; }
public VersionStamp Version { get; }
// Computed State
public ProjectEngineTracker ProjectEngine
{
get
{
if (_projectEngine == null)
{
lock (_lock)
{
if (_projectEngine == null)
{
_projectEngine = new ProjectEngineTracker(this);
}
}
}
return _projectEngine;
}
}
// Computed State
public ProjectTagHelperTracker TagHelpers
{
get
{
if (_tagHelpers == null)
{
lock (_lock)
{
if (_tagHelpers == null)
{
_tagHelpers = new ProjectTagHelperTracker(this);
}
}
}
return _tagHelpers;
}
}
public ProjectState AddHostDocument(HostDocument hostDocument)
{
if (hostDocument == null)
{
throw new ArgumentNullException(nameof(hostDocument));
}
// Ignore attempts to 'add' a document with different data, we only
// care about one, so it might as well be the one we have.
if (Documents.ContainsKey(hostDocument.FilePath))
{
return this;
}
var documents = new Dictionary<string, DocumentState>(FilePathComparer.Instance);
foreach (var kvp in Documents)
{
documents.Add(kvp.Key, kvp.Value);
}
documents.Add(hostDocument.FilePath, new DocumentState(Services, hostDocument));
var difference = ProjectDifference.DocumentsChanged;
var state = new ProjectState(this, difference, HostProject, WorkspaceProject, documents);
return state;
}
public ProjectState RemoveHostDocument(HostDocument hostDocument)
{
if (hostDocument == null)
{
throw new ArgumentNullException(nameof(hostDocument));
}
if (!Documents.ContainsKey(hostDocument.FilePath))
{
return this;
}
var documents = new Dictionary<string, DocumentState>(FilePathComparer.Instance);
foreach (var kvp in Documents)
{
documents.Add(kvp.Key, kvp.Value);
}
documents.Remove(hostDocument.FilePath);
var difference = ProjectDifference.DocumentsChanged;
var state = new ProjectState(this, difference, HostProject, WorkspaceProject, documents);
return state;
}
public ProjectState WithHostProject(HostProject hostProject)
{
if (hostProject == null)
{
throw new ArgumentNullException(nameof(hostProject));
}
if (HostProject.Configuration.Equals(hostProject.Configuration))
{
return this;
}
var difference = ProjectDifference.ConfigurationChanged;
var documents = new Dictionary<string, DocumentState>(FilePathComparer.Instance);
foreach (var kvp in Documents)
{
documents.Add(kvp.Key, new DocumentState(kvp.Value, difference));
}
var state = new ProjectState(this, difference, hostProject, WorkspaceProject, documents);
return state;
}
public ProjectState WithWorkspaceProject(Project workspaceProject)
{
var difference = ProjectDifference.None;
if (WorkspaceProject == null && workspaceProject != null)
{
difference |= ProjectDifference.WorkspaceProjectAdded;
}
else if (WorkspaceProject != null && workspaceProject == null)
{
difference |= ProjectDifference.WorkspaceProjectRemoved;
}
else if (
WorkspaceProject?.Id != workspaceProject?.Id ||
WorkspaceProject?.Version != workspaceProject?.Version)
{
// For now this is very naive. We will want to consider changing
// our logic here to be more robust.
difference |= ProjectDifference.WorkspaceProjectChanged;
}
if (difference == ProjectDifference.None)
{
return this;
}
var documents = new Dictionary<string, DocumentState>(FilePathComparer.Instance);
foreach (var kvp in Documents)
{
documents.Add(kvp.Key, new DocumentState(kvp.Value, difference));
}
var state = new ProjectState(this, difference, HostProject, workspaceProject, documents);
return state;
}
}
}

View File

@ -0,0 +1,79 @@
// 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.Threading.Tasks;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.CodeAnalysis.Host;
namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
{
internal class ProjectTagHelperTracker
{
private const ProjectDifference Mask =
ProjectDifference.ConfigurationChanged |
ProjectDifference.WorkspaceProjectAdded |
ProjectDifference.WorkspaceProjectChanged |
ProjectDifference.WorkspaceProjectRemoved;
private readonly object _lock = new object();
private readonly HostWorkspaceServices _services;
private Task<IReadOnlyList<TagHelperDescriptor>> _task;
public ProjectTagHelperTracker(ProjectState state)
{
if (state == null)
{
throw new ArgumentNullException(nameof(state));
}
_services = state.Services;
}
public bool IsResultAvailable => _task?.IsCompleted == true;
public ProjectTagHelperTracker ForkFor(ProjectState state, ProjectDifference difference)
{
if (state == null)
{
throw new ArgumentNullException(nameof(state));
}
if ((difference & Mask) != 0)
{
return null;
}
return this;
}
public Task<IReadOnlyList<TagHelperDescriptor>> GetTagHelperInitializationTask(ProjectSnapshot snapshot)
{
if (snapshot == null)
{
throw new ArgumentNullException(nameof(snapshot));
}
if (_task == null)
{
lock (_lock)
{
if (_task == null)
{
_task = GetTagHelperInitializationTaskCore(snapshot);
}
}
}
return _task;
}
private async Task<IReadOnlyList<TagHelperDescriptor>> GetTagHelperInitializationTaskCore(ProjectSnapshot snapshot)
{
var resolver = _services.GetLanguageServices(RazorLanguage.Name).GetRequiredService<TagHelperResolver>();
return (await resolver.GetTagHelpersAsync(snapshot)).Descriptors;
}
}
}

View File

@ -1,23 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
namespace Microsoft.CodeAnalysis.Razor
{
internal abstract class RazorProjectEngineFactoryService : ILanguageService
{
public abstract IProjectEngineFactory FindFactory(ProjectSnapshot project);
public abstract IProjectEngineFactory FindSerializableFactory(ProjectSnapshot project);
public abstract RazorProjectEngine Create(ProjectSnapshot project, Action<RazorProjectEngineBuilder> configure);
public abstract RazorProjectEngine Create(ProjectSnapshot project, RazorProjectFileSystem fileSystem, Action<RazorProjectEngineBuilder> configure);
public abstract RazorProjectEngine Create(string directoryPath, Action<RazorProjectEngineBuilder> configure);
}
}

View File

@ -1,10 +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.
namespace Microsoft.CodeAnalysis.Remote.Razor
{
internal class GeneratedDocument
{
public string Text { get; set; }
}
}

View File

@ -2,13 +2,9 @@
// 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.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.CodeAnalysis.Razor;
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
@ -27,42 +23,5 @@ namespace Microsoft.CodeAnalysis.Remote.Razor
return await RazorServices.TagHelperResolver.GetTagHelpersAsync(project, factoryTypeName, cancellationToken);
}
public Task<IEnumerable<DirectiveDescriptor>> GetDirectivesAsync(Guid projectIdBytes, string projectDebugName, CancellationToken cancellationToken = default(CancellationToken))
{
var projectId = ProjectId.CreateFromSerialized(projectIdBytes, projectDebugName);
var projectEngine = RazorProjectEngine.Create();
var directives = projectEngine.EngineFeatures.OfType<IRazorDirectiveFeature>().FirstOrDefault()?.Directives;
return Task.FromResult(directives ?? Enumerable.Empty<DirectiveDescriptor>());
}
public Task<GeneratedDocument> GenerateDocumentAsync(Guid projectIdBytes, string projectDebugName, string filePath, string text, CancellationToken cancellationToken = default(CancellationToken))
{
var projectId = ProjectId.CreateFromSerialized(projectIdBytes, projectDebugName);
var projectEngine = RazorProjectEngine.Create();
RazorSourceDocument source;
using (var stream = new MemoryStream())
{
var bytes = Encoding.UTF8.GetBytes(text);
stream.Write(bytes, 0, bytes.Length);
stream.Seek(0L, SeekOrigin.Begin);
source = RazorSourceDocument.ReadFrom(stream, filePath, Encoding.UTF8);
}
var code = RazorCodeDocument.Create(source);
projectEngine.Engine.Process(code);
var csharp = code.GetCSharpDocument();
if (csharp == null)
{
throw new InvalidOperationException();
}
return Task.FromResult(new GeneratedDocument() { Text = csharp.GeneratedCode, });
}
}
}

View File

@ -48,9 +48,7 @@ namespace Microsoft.CodeAnalysis.Remote.Razor
{
FilePath = filePath;
Configuration = configuration;
HostProject = new HostProject(filePath, configuration);
WorkspaceProject = workspaceProject;
TagHelpers = Array.Empty<TagHelperDescriptor>();
IsInitialized = true;
Version = VersionStamp.Default;
@ -58,6 +56,8 @@ namespace Microsoft.CodeAnalysis.Remote.Razor
public override RazorConfiguration Configuration { get; }
public override IEnumerable<string> DocumentFilePaths => Array.Empty<string>();
public override string FilePath { get; }
public override bool IsInitialized { get; }
@ -66,9 +66,30 @@ namespace Microsoft.CodeAnalysis.Remote.Razor
public override Project WorkspaceProject { get; }
public override HostProject HostProject { get; }
public override DocumentSnapshot GetDocument(string filePath)
{
if (filePath == null)
{
throw new ArgumentNullException(nameof(filePath));
}
public override IReadOnlyList<TagHelperDescriptor> TagHelpers { get; }
return null;
}
public override RazorProjectEngine GetProjectEngine()
{
throw new NotImplementedException();
}
public override Task<IReadOnlyList<TagHelperDescriptor>> GetTagHelpersAsync()
{
throw new NotImplementedException();
}
public override bool TryGetTagHelpers(out IReadOnlyList<TagHelperDescriptor> results)
{
throw new NotImplementedException();
}
}
}
}

View File

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<Rule
Description="Razor Document Properties"
DisplayName="Razor Document Properties"
Name="RazorGenerateWithTargetPath"
PageTemplate="generic"
xmlns="http://schemas.microsoft.com/build/2009/properties">
<Rule.DataSource>
<DataSource
Persistence="ProjectFile"
ItemType="RazorGenerateWithTargetPath"
MSBuildTarget="RazorGenerateDesignTime"
HasConfigurationCondition="False"
SourceOfDefaultValue="AfterContext"
SourceType="TargetResults" />
</Rule.DataSource>
<Rule.Categories>
<Category
Name="General"
DisplayName="General" />
</Rule.Categories>
<StringProperty
Category="General"
Name="TargetPath"
ReadOnly="True"
Visible="False" />
</Rule>

View File

@ -16,7 +16,6 @@ namespace Microsoft.VisualStudio.Editor.Razor
private readonly FileChangeTrackerFactory _fileChangeTrackerFactory;
private readonly ForegroundDispatcher _foregroundDispatcher;
private readonly ErrorReporter _errorReporter;
private readonly RazorProjectEngineFactoryService _projectEngineFactoryService;
private readonly Dictionary<string, ImportTracker> _importTrackerCache;
public override event EventHandler<ImportChangedEventArgs> Changed;
@ -24,8 +23,7 @@ namespace Microsoft.VisualStudio.Editor.Razor
public DefaultImportDocumentManager(
ForegroundDispatcher foregroundDispatcher,
ErrorReporter errorReporter,
FileChangeTrackerFactory fileChangeTrackerFactory,
RazorProjectEngineFactoryService projectEngineFactoryService)
FileChangeTrackerFactory fileChangeTrackerFactory)
{
if (foregroundDispatcher == null)
{
@ -42,15 +40,9 @@ namespace Microsoft.VisualStudio.Editor.Razor
throw new ArgumentNullException(nameof(fileChangeTrackerFactory));
}
if (projectEngineFactoryService == null)
{
throw new ArgumentNullException(nameof(projectEngineFactoryService));
}
_foregroundDispatcher = foregroundDispatcher;
_errorReporter = errorReporter;
_fileChangeTrackerFactory = fileChangeTrackerFactory;
_projectEngineFactoryService = projectEngineFactoryService;
_importTrackerCache = new Dictionary<string, ImportTracker>(StringComparer.OrdinalIgnoreCase);
}
@ -115,8 +107,7 @@ namespace Microsoft.VisualStudio.Editor.Razor
private IEnumerable<RazorProjectItem> GetImportItems(VisualStudioDocumentTracker tracker)
{
var projectDirectory = Path.GetDirectoryName(tracker.ProjectPath);
var projectEngine = _projectEngineFactoryService.Create(projectDirectory, _ => { });
var projectEngine = tracker.ProjectSnapshot.GetProjectEngine();
var trackerItem = projectEngine.FileSystem.GetItem(tracker.FilePath);
var importFeature = projectEngine.ProjectFeatures.OfType<IImportProjectFeature>().FirstOrDefault();

View File

@ -35,13 +35,8 @@ namespace Microsoft.VisualStudio.Editor.Razor
var errorReporter = languageServices.WorkspaceServices.GetRequiredService<ErrorReporter>();
var fileChangeTrackerFactory = languageServices.GetRequiredService<FileChangeTrackerFactory>();
var projectEngineFactoryService = languageServices.GetRequiredService<RazorProjectEngineFactoryService>();
return new DefaultImportDocumentManager(
_foregroundDispatcher,
errorReporter,
fileChangeTrackerFactory,
projectEngineFactoryService);
return new DefaultImportDocumentManager(_foregroundDispatcher, errorReporter, fileChangeTrackerFactory);
}
}
}

View File

@ -1,196 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.IO;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Razor;
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
namespace Microsoft.VisualStudio.Editor.Razor
{
internal class DefaultProjectEngineFactoryService : RazorProjectEngineFactoryService
{
private readonly static RazorConfiguration DefaultConfiguration = FallbackRazorConfiguration.MVC_2_1;
private readonly Workspace _workspace;
private readonly IFallbackProjectEngineFactory _defaultFactory;
private readonly Lazy<IProjectEngineFactory, ICustomProjectEngineFactoryMetadata>[] _customFactories;
private ProjectSnapshotManager _projectManager;
public DefaultProjectEngineFactoryService(
Workspace workspace,
IFallbackProjectEngineFactory defaultFactory,
Lazy<IProjectEngineFactory, ICustomProjectEngineFactoryMetadata>[] customFactories)
{
if (workspace == null)
{
throw new ArgumentNullException(nameof(workspace));
}
if (defaultFactory == null)
{
throw new ArgumentNullException(nameof(defaultFactory));
}
if (customFactories == null)
{
throw new ArgumentNullException(nameof(customFactories));
}
_workspace = workspace;
_defaultFactory = defaultFactory;
_customFactories = customFactories;
}
// Internal for testing
internal DefaultProjectEngineFactoryService(
ProjectSnapshotManager projectManager,
IFallbackProjectEngineFactory defaultFactory,
Lazy<IProjectEngineFactory, ICustomProjectEngineFactoryMetadata>[] customFactories)
{
if (projectManager == null)
{
throw new ArgumentNullException(nameof(projectManager));
}
if (defaultFactory == null)
{
throw new ArgumentNullException(nameof(defaultFactory));
}
if (customFactories == null)
{
throw new ArgumentNullException(nameof(customFactories));
}
_projectManager = projectManager;
_defaultFactory = defaultFactory;
_customFactories = customFactories;
}
public override IProjectEngineFactory FindFactory(ProjectSnapshot project)
{
if (project == null)
{
throw new ArgumentNullException(nameof(project));
}
return SelectFactory(project.Configuration ?? DefaultConfiguration, requireSerializable: false);
}
public override IProjectEngineFactory FindSerializableFactory(ProjectSnapshot project)
{
if (project == null)
{
throw new ArgumentNullException(nameof(project));
}
return SelectFactory(project.Configuration ?? DefaultConfiguration, requireSerializable: true);
}
public override RazorProjectEngine Create(ProjectSnapshot project, Action<RazorProjectEngineBuilder> configure)
{
if (project == null)
{
throw new ArgumentNullException(nameof(project));
}
return CreateCore(project, RazorProjectFileSystem.Create(Path.GetDirectoryName(project.FilePath)), configure);
}
public override RazorProjectEngine Create(string directoryPath, Action<RazorProjectEngineBuilder> configure)
{
if (directoryPath == null)
{
throw new ArgumentNullException(nameof(directoryPath));
}
var project = FindProjectByDirectory(directoryPath);
return CreateCore(project, RazorProjectFileSystem.Create(directoryPath), configure);
}
public override RazorProjectEngine Create(ProjectSnapshot project, RazorProjectFileSystem fileSystem, Action<RazorProjectEngineBuilder> configure)
{
if (project == null)
{
throw new ArgumentNullException(nameof(project));
}
if (fileSystem == null)
{
throw new ArgumentNullException(nameof(fileSystem));
}
return CreateCore(project, fileSystem, configure);
}
private RazorProjectEngine CreateCore(ProjectSnapshot project, RazorProjectFileSystem fileSystem, Action<RazorProjectEngineBuilder> configure)
{
// When we're running in the editor, the editor provides a configure delegate that will include
// the editor settings and tag helpers.
//
// This service is only used in process in Visual Studio, and any other callers should provide these
// things also.
configure = configure ?? ((b) => { });
// The default configuration currently matches the newest MVC configuration.
//
// We typically want this because the language adds features over time - we don't want to a bunch of errors
// to show up when a document is first opened, and then go away when the configuration loads, we'd prefer the opposite.
var configuration = project?.Configuration ?? DefaultConfiguration;
// If there's no factory to handle the configuration then fall back to a very basic configuration.
//
// This will stop a crash from happening in this case (misconfigured project), but will still make
// it obvious to the user that something is wrong.
var factory = SelectFactory(configuration) ?? _defaultFactory;
return factory.Create(configuration, fileSystem, configure);
}
private IProjectEngineFactory SelectFactory(RazorConfiguration configuration, bool requireSerializable = false)
{
for (var i = 0; i < _customFactories.Length; i++)
{
var factory = _customFactories[i];
if (string.Equals(configuration.ConfigurationName, factory.Metadata.ConfigurationName))
{
return requireSerializable && !factory.Metadata.SupportsSerialization ? null : factory.Value;
}
}
return null;
}
private ProjectSnapshot FindProjectByDirectory(string directory)
{
directory = NormalizeDirectoryPath(directory);
if (_projectManager == null)
{
_projectManager = _workspace.Services.GetLanguageServices(RazorLanguage.Name).GetRequiredService<ProjectSnapshotManager>();
}
var projects = _projectManager.Projects;
for (var i = 0; i < projects.Count; i++)
{
var project = projects[i];
if (project.FilePath != null)
{
if (string.Equals(directory, NormalizeDirectoryPath(Path.GetDirectoryName(project.FilePath)), StringComparison.OrdinalIgnoreCase))
{
return project;
}
}
}
return null;
}
private string NormalizeDirectoryPath(string path)
{
return path.Replace('\\', '/').TrimEnd('/');
}
}
}

View File

@ -1,48 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Composition;
using System.Linq;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.Razor;
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
namespace Microsoft.VisualStudio.Editor.Razor
{
[ExportLanguageServiceFactory(typeof(RazorProjectEngineFactoryService), RazorLanguage.Name, ServiceLayer.Default)]
internal class DefaultProjectEngineFactoryServiceFactory : ILanguageServiceFactory
{
private readonly Lazy<IProjectEngineFactory, ICustomProjectEngineFactoryMetadata>[] _customFactories;
private readonly IFallbackProjectEngineFactory _fallbackFactory;
[ImportingConstructor]
public DefaultProjectEngineFactoryServiceFactory(
IFallbackProjectEngineFactory fallbackFactory,
[ImportMany] IEnumerable<Lazy<IProjectEngineFactory, ICustomProjectEngineFactoryMetadata>> customFactories)
{
if (fallbackFactory == null)
{
throw new ArgumentNullException(nameof(fallbackFactory));
}
if (customFactories == null)
{
throw new ArgumentNullException(nameof(customFactories));
}
_fallbackFactory = fallbackFactory;
_customFactories = customFactories.ToArray();
}
public ILanguageService CreateLanguageService(HostLanguageServices languageServices)
{
return new DefaultProjectEngineFactoryService(
languageServices.WorkspaceServices.Workspace,
_fallbackFactory,
_customFactories);
}
}
}

View File

@ -4,7 +4,6 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.CodeAnalysis.Razor;
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
@ -12,18 +11,6 @@ namespace Microsoft.VisualStudio.Editor.Razor
{
internal class DefaultTagHelperResolver : TagHelperResolver
{
private readonly RazorProjectEngineFactoryService _engineFactory;
public DefaultTagHelperResolver(RazorProjectEngineFactoryService engineFactory)
{
if (engineFactory == null)
{
throw new ArgumentNullException(nameof(engineFactory));
}
_engineFactory = engineFactory;
}
public override Task<TagHelperResolutionResult> GetTagHelpersAsync(ProjectSnapshot project, CancellationToken cancellationToken = default)
{
if (project == null)
@ -35,9 +22,8 @@ namespace Microsoft.VisualStudio.Editor.Razor
{
return Task.FromResult(TagHelperResolutionResult.Empty);
}
var engine = _engineFactory.Create(project, RazorProjectFileSystem.Empty, b => { });
return GetTagHelpersAsync(project, engine);
return GetTagHelpersAsync(project, project.GetProjectEngine());
}
}
}

View File

@ -14,7 +14,7 @@ namespace Microsoft.VisualStudio.Editor.Razor
{
public ILanguageService CreateLanguageService(HostLanguageServices languageServices)
{
return new DefaultTagHelperResolver(languageServices.GetRequiredService<RazorProjectEngineFactoryService>());
return new DefaultTagHelperResolver();
}
}
}

View File

@ -3,6 +3,9 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Razor;
@ -25,7 +28,13 @@ namespace Microsoft.VisualStudio.Editor.Razor
private readonly List<ITextView> _textViews;
private readonly Workspace _workspace;
private bool _isSupportedProject;
private ProjectSnapshot _project;
private ProjectSnapshot _projectSnapshot;
// Only allow a single tag helper computation task at a time.
private (ProjectSnapshot project, Task task) _computingTagHelpers;
// Stores the result from the last time we computed tag helpers.
private IReadOnlyList<TagHelperDescriptor> _tagHelpers;
public override event EventHandler<ContextChangeEventArgs> ContextChanged;
@ -89,17 +98,23 @@ namespace Microsoft.VisualStudio.Editor.Razor
_workspace = workspace; // For now we assume that the workspace is the always default VS workspace.
_textViews = new List<ITextView>();
_tagHelpers = Array.Empty<TagHelperDescriptor>();
}
public override RazorConfiguration Configuration => _project?.Configuration;
public override RazorConfiguration Configuration => _projectSnapshot?.Configuration;
public override EditorSettings EditorSettings => _workspaceEditorSettings.Current;
public override IReadOnlyList<TagHelperDescriptor> TagHelpers => _project?.TagHelpers ?? Array.Empty<TagHelperDescriptor>();
public override IReadOnlyList<TagHelperDescriptor> TagHelpers => _tagHelpers;
public override bool IsSupportedProject => _isSupportedProject;
public override Project Project => _workspace.CurrentSolution.GetProject(_project.WorkspaceProject.Id);
public override Project Project =>
_projectSnapshot.WorkspaceProject == null ?
null :
_workspace.CurrentSolution.GetProject(_projectSnapshot.WorkspaceProject.Id);
internal override ProjectSnapshot ProjectSnapshot => _projectSnapshot;
public override ITextBuffer TextBuffer => _textBuffer;
@ -111,6 +126,8 @@ namespace Microsoft.VisualStudio.Editor.Razor
public override string ProjectPath => _projectPath;
public Task PendingTagHelperTask => _computingTagHelpers.task ?? Task.CompletedTask;
internal void AddTextView(ITextView textView)
{
if (textView == null)
@ -118,6 +135,8 @@ namespace Microsoft.VisualStudio.Editor.Razor
throw new ArgumentNullException(nameof(textView));
}
_foregroundDispatcher.AssertForegroundThread();
if (!_textViews.Contains(textView))
{
_textViews.Add(textView);
@ -131,6 +150,8 @@ namespace Microsoft.VisualStudio.Editor.Razor
throw new ArgumentNullException(nameof(textView));
}
_foregroundDispatcher.AssertForegroundThread();
if (_textViews.Contains(textView))
{
_textViews.Remove(textView);
@ -139,6 +160,8 @@ namespace Microsoft.VisualStudio.Editor.Razor
public override ITextView GetFocusedTextView()
{
_foregroundDispatcher.AssertForegroundThread();
for (var i = 0; i < TextViews.Count; i++)
{
if (TextViews[i].HasAggregateFocus)
@ -152,20 +175,23 @@ namespace Microsoft.VisualStudio.Editor.Razor
public void Subscribe()
{
_foregroundDispatcher.AssertForegroundThread();
_projectSnapshot = _projectManager.GetOrCreateProject(_projectPath);
_isSupportedProject = true;
_projectManager.Changed += ProjectManager_Changed;
_workspaceEditorSettings.Changed += EditorSettingsManager_Changed;
_importDocumentManager.Changed += Import_Changed;
_importDocumentManager.OnSubscribed(this);
_workspaceEditorSettings.Changed += EditorSettingsManager_Changed;
_projectManager.Changed += ProjectManager_Changed;
_importDocumentManager.Changed += Import_Changed;
_isSupportedProject = true;
_project = _projectManager.GetProjectWithFilePath(_projectPath);
OnContextChanged(_project, ContextChangeKind.ProjectChanged);
OnContextChanged(ContextChangeKind.ProjectChanged);
}
public void Unsubscribe()
{
_foregroundDispatcher.AssertForegroundThread();
_importDocumentManager.OnUnsubscribed(this);
_projectManager.Changed -= ProjectManager_Changed;
@ -174,37 +200,112 @@ namespace Microsoft.VisualStudio.Editor.Razor
// Detached from project.
_isSupportedProject = false;
_project = null;
OnContextChanged(project: null, kind: ContextChangeKind.ProjectChanged);
_projectSnapshot = null;
OnContextChanged(kind: ContextChangeKind.ProjectChanged);
}
private void OnContextChanged(ProjectSnapshot project, ContextChangeKind kind)
private void StartComputingTagHelpers()
{
_foregroundDispatcher.AssertForegroundThread();
_project = project;
Debug.Assert(_projectSnapshot != null);
Debug.Assert(_computingTagHelpers.project == null && _computingTagHelpers.task == null);
if (_projectSnapshot.TryGetTagHelpers(out var results))
{
_tagHelpers = results;
OnContextChanged(ContextChangeKind.TagHelpersChanged);
return;
}
// if we get here then we know the tag helpers aren't available, so force async for ease of testing
var task = _projectSnapshot
.GetTagHelpersAsync()
.ContinueWith(TagHelpersUpdated, CancellationToken.None, TaskContinuationOptions.RunContinuationsAsynchronously, _foregroundDispatcher.ForegroundScheduler);
_computingTagHelpers = (_projectSnapshot, task);
}
private void TagHelpersUpdated(Task<IReadOnlyList<TagHelperDescriptor>> task)
{
_foregroundDispatcher.AssertForegroundThread();
Debug.Assert(_computingTagHelpers.project != null && _computingTagHelpers.task != null);
if (!_isSupportedProject)
{
return;
}
_tagHelpers = task.Exception == null ? task.Result : Array.Empty<TagHelperDescriptor>();
OnContextChanged(ContextChangeKind.TagHelpersChanged);
var projectHasChanges = _projectSnapshot != null && _projectSnapshot != _computingTagHelpers.project;
_computingTagHelpers = (null, null);
if (projectHasChanges)
{
// More changes, keep going.
StartComputingTagHelpers();
}
}
private void OnContextChanged(ContextChangeKind kind)
{
_foregroundDispatcher.AssertForegroundThread();
var handler = ContextChanged;
if (handler != null)
{
handler(this, new ContextChangeEventArgs(kind));
}
if (kind == ContextChangeKind.ProjectChanged &&
_projectSnapshot != null &&
_computingTagHelpers.project == null)
{
StartComputingTagHelpers();
}
}
// Internal for testing
internal void ProjectManager_Changed(object sender, ProjectChangeEventArgs e)
{
_foregroundDispatcher.AssertForegroundThread();
if (_projectPath != null &&
string.Equals(_projectPath, e.Project.FilePath, StringComparison.OrdinalIgnoreCase))
string.Equals(_projectPath, e.ProjectFilePath, StringComparison.OrdinalIgnoreCase))
{
if (e.Kind == ProjectChangeKind.TagHelpersChanged)
// This will be the new snapshot unless the project was removed.
_projectSnapshot = _projectManager.GetLoadedProject(e.ProjectFilePath);
switch (e.Kind)
{
OnContextChanged(e.Project, ContextChangeKind.TagHelpersChanged);
}
else
{
OnContextChanged(e.Project, ContextChangeKind.ProjectChanged);
case ProjectChangeKind.DocumentsChanged:
// Nothing to do.
break;
case ProjectChangeKind.ProjectAdded:
case ProjectChangeKind.ProjectChanged:
// Just an update
OnContextChanged(ContextChangeKind.ProjectChanged);
break;
case ProjectChangeKind.ProjectRemoved:
// Fall back to ephemeral project
_projectSnapshot = _projectManager.GetOrCreateProject(ProjectPath);
OnContextChanged(ContextChangeKind.ProjectChanged);
break;
case ProjectChangeKind.DocumentContentChanged:
// Do nothing
break;
default:
throw new InvalidOperationException($"Unknown ProjectChangeKind {e.Kind}");
}
}
}
@ -212,17 +313,21 @@ namespace Microsoft.VisualStudio.Editor.Razor
// Internal for testing
internal void EditorSettingsManager_Changed(object sender, EditorSettingsChangedEventArgs args)
{
OnContextChanged(_project, ContextChangeKind.EditorSettingsChanged);
_foregroundDispatcher.AssertForegroundThread();
OnContextChanged(ContextChangeKind.EditorSettingsChanged);
}
// Internal for testing
internal void Import_Changed(object sender, ImportChangedEventArgs args)
{
_foregroundDispatcher.AssertForegroundThread();
foreach (var path in args.AssociatedDocuments)
{
if (string.Equals(_filePath, path, StringComparison.OrdinalIgnoreCase))
{
OnContextChanged(_project, ContextChangeKind.ImportsChanged);
OnContextChanged(ContextChangeKind.ImportsChanged);
break;
}
}

View File

@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
@ -31,7 +32,7 @@ namespace Microsoft.VisualStudio.Editor.Razor
private readonly VisualStudioCompletionBroker _completionBroker;
private readonly VisualStudioDocumentTracker _documentTracker;
private readonly ForegroundDispatcher _dispatcher;
private readonly RazorProjectEngineFactoryService _projectEngineFactory;
private readonly ProjectSnapshotProjectEngineFactory _projectEngineFactory;
private readonly ErrorReporter _errorReporter;
private RazorProjectEngine _projectEngine;
private RazorCodeDocument _codeDocument;
@ -47,7 +48,7 @@ namespace Microsoft.VisualStudio.Editor.Razor
public DefaultVisualStudioRazorParser(
ForegroundDispatcher dispatcher,
VisualStudioDocumentTracker documentTracker,
RazorProjectEngineFactoryService projectEngineFactory,
ProjectSnapshotProjectEngineFactory projectEngineFactory,
ErrorReporter errorReporter,
VisualStudioCompletionBroker completionBroker)
{
@ -167,8 +168,18 @@ namespace Microsoft.VisualStudio.Editor.Razor
{
_dispatcher.AssertForegroundThread();
// Make sure any tests use the real thing or a good mock. These tests can cause failures
// that are hard to understand when this throws.
Debug.Assert(_documentTracker.IsSupportedProject);
Debug.Assert(_documentTracker.ProjectSnapshot != null);
_projectEngine = _projectEngineFactory.Create(_documentTracker.ProjectSnapshot, ConfigureProjectEngine);
Debug.Assert(_projectEngine != null);
Debug.Assert(_projectEngine.Engine != null);
Debug.Assert(_projectEngine.FileSystem != null);
var projectDirectory = Path.GetDirectoryName(_documentTracker.ProjectPath);
_projectEngine = _projectEngineFactory.Create(projectDirectory, ConfigureProjectEngine);
_parser = new BackgroundParser(_projectEngine, FilePath, projectDirectory);
_parser.ResultsReady += OnResultsReady;
_parser.Start();

View File

@ -9,7 +9,7 @@ namespace Microsoft.VisualStudio.Editor.Razor
internal class DefaultVisualStudioRazorParserFactory : VisualStudioRazorParserFactory
{
private readonly ForegroundDispatcher _dispatcher;
private readonly RazorProjectEngineFactoryService _projectEngineFactoryService;
private readonly ProjectSnapshotProjectEngineFactory _projectEngineFactory;
private readonly VisualStudioCompletionBroker _completionBroker;
private readonly ErrorReporter _errorReporter;
@ -17,7 +17,7 @@ namespace Microsoft.VisualStudio.Editor.Razor
ForegroundDispatcher dispatcher,
ErrorReporter errorReporter,
VisualStudioCompletionBroker completionBroker,
RazorProjectEngineFactoryService projectEngineFactoryService)
ProjectSnapshotProjectEngineFactory projectEngineFactory)
{
if (dispatcher == null)
{
@ -34,15 +34,15 @@ namespace Microsoft.VisualStudio.Editor.Razor
throw new ArgumentNullException(nameof(completionBroker));
}
if (projectEngineFactoryService == null)
if (projectEngineFactory == null)
{
throw new ArgumentNullException(nameof(projectEngineFactoryService));
throw new ArgumentNullException(nameof(projectEngineFactory));
}
_dispatcher = dispatcher;
_errorReporter = errorReporter;
_completionBroker = completionBroker;
_projectEngineFactoryService = projectEngineFactoryService;
_projectEngineFactory = projectEngineFactory;
}
public override VisualStudioRazorParser Create(VisualStudioDocumentTracker documentTracker)
@ -57,7 +57,7 @@ namespace Microsoft.VisualStudio.Editor.Razor
var parser = new DefaultVisualStudioRazorParser(
_dispatcher,
documentTracker,
_projectEngineFactoryService,
_projectEngineFactory,
_errorReporter,
_completionBroker);
return parser;

View File

@ -35,13 +35,13 @@ namespace Microsoft.VisualStudio.Editor.Razor
var workspaceServices = languageServices.WorkspaceServices;
var errorReporter = workspaceServices.GetRequiredService<ErrorReporter>();
var completionBroker = languageServices.GetRequiredService<VisualStudioCompletionBroker>();
var projectEngineFactoryService = languageServices.GetRequiredService<RazorProjectEngineFactoryService>();
var projectEngineFactory = workspaceServices.GetRequiredService<ProjectSnapshotProjectEngineFactory>();
return new DefaultVisualStudioRazorParserFactory(
_foregroundDispatcher,
errorReporter,
completionBroker,
projectEngineFactoryService);
projectEngineFactory);
}
}
}

View File

@ -6,6 +6,7 @@ using System.Collections.Generic;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Razor.Editor;
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Editor;
@ -29,6 +30,8 @@ namespace Microsoft.VisualStudio.Editor.Razor
public abstract Project Project { get; }
internal abstract ProjectSnapshot ProjectSnapshot { get; }
public abstract Workspace Workspace { get; }
public abstract ITextBuffer TextBuffer { get; }

View File

@ -1,41 +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.
#if RAZOR_EXTENSION_DEVELOPER_MODE
using System;
using System.Collections.Generic;
using System.Composition;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.CodeAnalysis;
namespace Microsoft.VisualStudio.LanguageServices.Razor
{
[Export(typeof(IRazorEngineDirectiveResolver))]
internal class DefaultRazorEngineDirectiveResolver : IRazorEngineDirectiveResolver
{
public async Task<IEnumerable<DirectiveDescriptor>> GetRazorEngineDirectivesAsync(Workspace workspace, Project project, CancellationToken cancellationToken = default(CancellationToken))
{
try
{
var client = await RazorLanguageServiceClientFactory.CreateAsync(workspace, cancellationToken);
using (var session = await client.CreateSessionAsync(project.Solution))
{
var directives = await session.InvokeAsync<IEnumerable<DirectiveDescriptor>>("GetDirectivesAsync", new object[] { project.Id.Id, "Foo", }, cancellationToken).ConfigureAwait(false);
return directives;
}
}
catch (Exception exception)
{
throw new InvalidOperationException(
Resources.FormatUnexpectedException(
typeof(DefaultRazorEngineDirectiveResolver).FullName,
nameof(GetRazorEngineDirectivesAsync)),
exception);
}
}
}
}
#endif

View File

@ -1,39 +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.
#if RAZOR_EXTENSION_DEVELOPER_MODE
using System;
using System.Composition;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
namespace Microsoft.VisualStudio.LanguageServices.Razor
{
[Export(typeof(IRazorEngineDocumentGenerator))]
internal class DefaultRazorEngineDocumentGenerator : IRazorEngineDocumentGenerator
{
public async Task<RazorEngineDocument> GenerateDocumentAsync(Workspace workspace, Project project, string filePath, string text, CancellationToken cancellationToken = default(CancellationToken))
{
try
{
var client = await RazorLanguageServiceClientFactory.CreateAsync(workspace, cancellationToken);
using (var session = await client.CreateSessionAsync(project.Solution))
{
var document = await session.InvokeAsync<RazorEngineDocument>("GenerateDocumentAsync", new object[] { project.Id.Id, "Foo", filePath, text }, cancellationToken).ConfigureAwait(false);
return document;
}
}
catch (Exception exception)
{
throw new InvalidOperationException(
Resources.FormatUnexpectedException(
typeof(DefaultRazorEngineDocumentGenerator).FullName,
nameof(GenerateDocumentAsync)),
exception);
}
}
}
}
#endif

View File

@ -1,18 +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.
#if RAZOR_EXTENSION_DEVELOPER_MODE
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.CodeAnalysis;
namespace Microsoft.VisualStudio.LanguageServices.Razor
{
internal interface IRazorEngineDirectiveResolver
{
Task<IEnumerable<DirectiveDescriptor>> GetRazorEngineDirectivesAsync(Workspace workspace, Project project, CancellationToken cancellationToken = default(CancellationToken));
}
}
#endif

View File

@ -1,16 +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.
#if RAZOR_EXTENSION_DEVELOPER_MODE
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
namespace Microsoft.VisualStudio.LanguageServices.Razor
{
internal interface IRazorEngineDocumentGenerator
{
Task<RazorEngineDocument> GenerateDocumentAsync(Workspace workspace, Project project, string filePath, string text, CancellationToken cancellationToken = default(CancellationToken));
}
}
#endif

View File

@ -43,7 +43,7 @@
<Reference Include="System.Xaml"/>
</ItemGroup>
<!--
<!--
The ProjectSystem.SDK tasks that handle XamlPropertyRule don't work on the dotnet core version
of MSBuild. The workaround here is to only hardcode the generated code location such that it gets
checked in. Then we don't need to generate it at build time.
@ -59,6 +59,9 @@
<None Include="$(RulesDirectory)RazorGeneral.xaml">
<Link>ProjectSystem\Rules\RazorGeneral.xaml</Link>
</None>
<None Include="$(RulesDirectory)RazorGenerateWithTargetPath.xaml">
<Link>ProjectSystem\Rules\RazorGenerateWithTargetPath.xaml</Link>
</None>
<EmbeddedResource Include="$(RulesDirectory)RazorConfiguration.xaml">
<LogicalName>XamlRuleToCode:RazorConfiguration.xaml</LogicalName>
</EmbeddedResource>
@ -68,6 +71,9 @@
<EmbeddedResource Include="$(RulesDirectory)RazorGeneral.xaml">
<LogicalName>XamlRuleToCode:RazorGeneral.xaml</LogicalName>
</EmbeddedResource>
<EmbeddedResource Include="$(RulesDirectory)RazorGenerateWithTargetPath.xaml">
<LogicalName>XamlRuleToCode:RazorGenerateWithTargetPath.xaml</LogicalName>
</EmbeddedResource>
</ItemGroup>
<ItemGroup Condition="'$(MSBuildRuntimeType)'!='Core'">
<XamlPropertyRule Include="$(RulesDirectory)RazorConfiguration.xaml">
@ -92,6 +98,13 @@
<RuleInjectionClassName>RazorProjectProperties</RuleInjectionClassName>
<OutputPath>ProjectSystem\Rules\</OutputPath>
</XamlPropertyRule>
<XamlPropertyRule Include="$(RulesDirectory)RazorGenerateWithTargetPath.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:GenerateRuleSourceFromXaml</Generator>
<Namespace>Microsoft.CodeAnalysis.Razor.ProjectSystem.Rules</Namespace>
<RuleInjectionClassName>RazorProjectProperties</RuleInjectionClassName>
<OutputPath>ProjectSystem\Rules\</OutputPath>
</XamlPropertyRule>
</ItemGroup>
<ItemGroup>
<Compile Update="ProjectSystem\Rules\RazorConfiguration.cs">
@ -103,6 +116,9 @@
<Compile Update="ProjectSystem\Rules\RazorGeneral.cs">
<DependentUpon>ProjectSystem\Rules\RazorGeneral.xaml</DependentUpon>
</Compile>
<Compile Update="ProjectSystem\Rules\RazorGenerateWithTargetPath.cs">
<DependentUpon>ProjectSystem\Rules\RazorGenerateWithTargetPath.xaml</DependentUpon>
</Compile>
</ItemGroup>
<!--

View File

@ -16,15 +16,15 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor
internal class OOPTagHelperResolver : TagHelperResolver
{
private readonly DefaultTagHelperResolver _defaultResolver;
private readonly RazorProjectEngineFactoryService _engineFactory;
private readonly ProjectSnapshotProjectEngineFactory _factory;
private readonly ErrorReporter _errorReporter;
private readonly Workspace _workspace;
public OOPTagHelperResolver(RazorProjectEngineFactoryService engineFactory, ErrorReporter errorReporter, Workspace workspace)
public OOPTagHelperResolver(ProjectSnapshotProjectEngineFactory factory, ErrorReporter errorReporter, Workspace workspace)
{
if (engineFactory == null)
if (factory == null)
{
throw new ArgumentNullException(nameof(engineFactory));
throw new ArgumentNullException(nameof(factory));
}
if (errorReporter == null)
@ -37,11 +37,11 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor
throw new ArgumentNullException(nameof(workspace));
}
_engineFactory = engineFactory;
_factory = factory;
_errorReporter = errorReporter;
_workspace = workspace;
_defaultResolver = new DefaultTagHelperResolver(_engineFactory);
_defaultResolver = new DefaultTagHelperResolver();
}
public override async Task<TagHelperResolutionResult> GetTagHelpersAsync(ProjectSnapshot project, CancellationToken cancellationToken = default)
@ -63,7 +63,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor
// 3. Use fallback factory in process
//
// Calling into RazorTemplateEngineFactoryService.Create will accomplish #2 and #3 in one step.
var factory = _engineFactory.FindSerializableFactory(project);
var factory = _factory.FindSerializableFactory(project);
try
{

View File

@ -1,21 +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;
using Microsoft.CodeAnalysis.Razor;
namespace Microsoft.VisualStudio.LanguageServices.Razor
{
[Shared]
[ExportLanguageServiceFactory(typeof(TagHelperResolver), RazorLanguage.Name, ServiceLayer.Host)]
internal class OOPTagHelperResolverFactory : ILanguageServiceFactory
{
public ILanguageService CreateLanguageService(HostLanguageServices languageServices)
{
return new OOPTagHelperResolver(
languageServices.GetRequiredService<RazorProjectEngineFactoryService>(),
languageServices.WorkspaceServices.GetRequiredService<ProjectSnapshotProjectEngineFactory>(),
languageServices.WorkspaceServices.GetRequiredService<ErrorReporter>(),
languageServices.WorkspaceServices.Workspace);
}

View File

@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.ComponentModel.Composition;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
@ -13,8 +14,7 @@ using Microsoft.AspNetCore.Razor.Language;
using Microsoft.VisualStudio.LanguageServices;
using Microsoft.VisualStudio.ProjectSystem;
using Microsoft.VisualStudio.ProjectSystem.Properties;
using ProjectState = System.Collections.Immutable.IImmutableDictionary<string, Microsoft.VisualStudio.ProjectSystem.Properties.IProjectRuleSnapshot>;
using ProjectStateItem = System.Collections.Generic.KeyValuePair<string, System.Collections.Immutable.IImmutableDictionary<string, string>>;
using Item = System.Collections.Generic.KeyValuePair<string, System.Collections.Immutable.IImmutableDictionary<string, string>>;
namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
{
@ -62,7 +62,13 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
receiver,
initialDataAsNew: true,
suppressVersionOnlyUpdates: true,
ruleNames: new string[] { Rules.RazorGeneral.SchemaName, Rules.RazorConfiguration.SchemaName, Rules.RazorExtension.SchemaName });
ruleNames: new string[]
{
Rules.RazorGeneral.SchemaName,
Rules.RazorConfiguration.SchemaName,
Rules.RazorExtension.SchemaName,
Rules.RazorGenerateWithTargetPath.SchemaName,
});
}
protected override async Task DisposeCoreAsync(bool initialized)
@ -90,12 +96,36 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
if (TryGetConfiguration(update.Value.CurrentState, out var configuration))
{
var hostProject = new HostProject(CommonServices.UnconfiguredProject.FullPath, configuration);
await UpdateProjectUnsafeAsync(hostProject).ConfigureAwait(false);
// We need to deal with the case where the project was uninitialized, but now
// is valid for Razor. In that case we might have previously seen all of the documents
// but ignored them because the project wasn't active.
//
// So what we do to deal with this, is that we 'remove' all changed and removed items
// and then we 'add' all current items. This allows minimal churn to the PSM, but still
// makes us up to date.
var documents = GetCurrentDocuments(update.Value);
var changedDocuments = GetChangedAndRemovedDocuments(update.Value);
await UpdateAsync(() =>
{
UpdateProjectUnsafe(hostProject);
for (var i = 0; i < changedDocuments.Length; i++)
{
RemoveDocumentUnsafe(changedDocuments[i]);
}
for (var i = 0; i < documents.Length; i++)
{
AddDocumentUnsafe(documents[i]);
}
}).ConfigureAwait(false);
}
else
{
// Ok we can't find a configuration. Let's assume this project isn't using Razor then.
await UpdateProjectUnsafeAsync(null).ConfigureAwait(false);
await UpdateAsync(UninitializeProjectUnsafe).ConfigureAwait(false);
}
});
}, registerFaultHandler: true);
@ -103,34 +133,34 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
// Internal for testing
internal static bool TryGetConfiguration(
ProjectState projectState,
IImmutableDictionary<string, IProjectRuleSnapshot> state,
out RazorConfiguration configuration)
{
if (!TryGetDefaultConfiguration(projectState, out var defaultConfiguration))
if (!TryGetDefaultConfiguration(state, out var defaultConfiguration))
{
configuration = null;
return false;
}
if (!TryGetLanguageVersion(projectState, out var languageVersion))
if (!TryGetLanguageVersion(state, out var languageVersion))
{
configuration = null;
return false;
}
if (!TryGetConfigurationItem(defaultConfiguration, projectState, out var configurationItem))
if (!TryGetConfigurationItem(defaultConfiguration, state, out var configurationItem))
{
configuration = null;
return false;
}
if (!TryGetConfiguredExtensionNames(configurationItem, out var configuredExtensionNames))
if (!TryGetExtensionNames(configurationItem, out var extensionNames))
{
configuration = null;
return false;
}
if (!TryGetExtensions(configuredExtensionNames, projectState, out var extensions))
if (!TryGetExtensions(extensionNames, state, out var extensions))
{
configuration = null;
return false;
@ -142,9 +172,11 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
// Internal for testing
internal static bool TryGetDefaultConfiguration(ProjectState projectState, out string defaultConfiguration)
internal static bool TryGetDefaultConfiguration(
IImmutableDictionary<string, IProjectRuleSnapshot> state,
out string defaultConfiguration)
{
if (!projectState.TryGetValue(Rules.RazorGeneral.SchemaName, out var rule))
if (!state.TryGetValue(Rules.RazorGeneral.SchemaName, out var rule))
{
defaultConfiguration = null;
return false;
@ -166,9 +198,11 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
}
// Internal for testing
internal static bool TryGetLanguageVersion(ProjectState projectState, out RazorLanguageVersion languageVersion)
internal static bool TryGetLanguageVersion(
IImmutableDictionary<string, IProjectRuleSnapshot> state,
out RazorLanguageVersion languageVersion)
{
if (!projectState.TryGetValue(Rules.RazorGeneral.SchemaName, out var rule))
if (!state.TryGetValue(Rules.RazorGeneral.SchemaName, out var rule))
{
languageVersion = null;
return false;
@ -197,17 +231,17 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
// Internal for testing
internal static bool TryGetConfigurationItem(
string configuration,
ProjectState projectState,
out ProjectStateItem configurationItem)
IImmutableDictionary<string, IProjectRuleSnapshot> state,
out Item configurationItem)
{
if (!projectState.TryGetValue(Rules.RazorConfiguration.PrimaryDataSourceItemType, out var configurationState))
if (!state.TryGetValue(Rules.RazorConfiguration.PrimaryDataSourceItemType, out var configurationState))
{
configurationItem = default(ProjectStateItem);
configurationItem = default(Item);
return false;
}
var razorConfigurationItems = configurationState.Items;
foreach (var item in razorConfigurationItems)
var items = configurationState.Items;
foreach (var item in items)
{
if (item.Key == configuration)
{
@ -216,44 +250,49 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
}
}
configurationItem = default(ProjectStateItem);
configurationItem = default(Item);
return false;
}
// Internal for testing
internal static bool TryGetConfiguredExtensionNames(ProjectStateItem configurationItem, out string[] configuredExtensionNames)
internal static bool TryGetExtensionNames(
Item configurationItem,
out string[] configuredExtensionNames)
{
if (!configurationItem.Value.TryGetValue(Rules.RazorConfiguration.ExtensionsProperty, out var extensionNamesValue))
if (!configurationItem.Value.TryGetValue(Rules.RazorConfiguration.ExtensionsProperty, out var extensionNames))
{
configuredExtensionNames = null;
return false;
}
if (string.IsNullOrEmpty(extensionNamesValue))
if (string.IsNullOrEmpty(extensionNames))
{
configuredExtensionNames = null;
return false;
}
configuredExtensionNames = extensionNamesValue.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries);
configuredExtensionNames = extensionNames.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries);
return true;
}
// Internal for testing
internal static bool TryGetExtensions(string[] configuredExtensionNames, ProjectState projectState, out ProjectSystemRazorExtension[] extensions)
internal static bool TryGetExtensions(
string[] extensionNames,
IImmutableDictionary<string, IProjectRuleSnapshot> state,
out ProjectSystemRazorExtension[] extensions)
{
if (!projectState.TryGetValue(Rules.RazorExtension.PrimaryDataSourceItemType, out var extensionState))
if (!state.TryGetValue(Rules.RazorExtension.PrimaryDataSourceItemType, out var rule))
{
extensions = null;
return false;
}
var extensionItems = extensionState.Items;
var items = rule.Items;
var extensionList = new List<ProjectSystemRazorExtension>();
foreach (var item in extensionItems)
foreach (var item in items)
{
var extensionName = item.Key;
if (configuredExtensionNames.Contains(extensionName))
if (extensionNames.Contains(extensionName))
{
extensionList.Add(new ProjectSystemRazorExtension(extensionName));
}
@ -262,5 +301,53 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
extensions = extensionList.ToArray();
return true;
}
private HostDocument[] GetCurrentDocuments(IProjectSubscriptionUpdate update)
{
if (!update.CurrentState.TryGetValue(Rules.RazorGenerateWithTargetPath.SchemaName, out var rule))
{
return Array.Empty<HostDocument>();
}
var documents = new List<HostDocument>();
foreach (var kvp in rule.Items)
{
if (kvp.Value.TryGetValue(Rules.RazorGenerateWithTargetPath.TargetPathProperty, out var targetPath) &&
!string.IsNullOrWhiteSpace(kvp.Key) &&
!string.IsNullOrWhiteSpace(targetPath))
{
var filePath = CommonServices.UnconfiguredProject.MakeRooted(kvp.Key);
documents.Add(new HostDocument(filePath, targetPath));
}
}
return documents.ToArray();
}
private HostDocument[] GetChangedAndRemovedDocuments(IProjectSubscriptionUpdate update)
{
if (!update.ProjectChanges.TryGetValue(Rules.RazorGenerateWithTargetPath.SchemaName, out var rule))
{
return Array.Empty<HostDocument>();
}
var documents = new List<HostDocument>();
foreach (var key in rule.Difference.RemovedItems.Concat(rule.Difference.ChangedItems))
{
if (rule.Before.Items.TryGetValue(key, out var value))
{
if (value.TryGetValue(Rules.RazorGenerateWithTargetPath.TargetPathProperty, out var targetPath) &&
!string.IsNullOrWhiteSpace(key) &&
!string.IsNullOrWhiteSpace(targetPath))
{
var filePath = CommonServices.UnconfiguredProject.MakeRooted(key);
documents.Add(new HostDocument(filePath, targetPath));
}
}
}
return documents.ToArray();
}
}
}

View File

@ -85,20 +85,23 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
await ExecuteWithLock(async () =>
{
string mvcReferenceFullPath = null;
var references = update.Value.CurrentState[ResolvedCompilationReference.SchemaName].Items;
foreach (var reference in references)
if (update.Value.CurrentState.ContainsKey(ResolvedCompilationReference.SchemaName))
{
if (reference.Key.EndsWith(MvcAssemblyFileName, StringComparison.OrdinalIgnoreCase))
var references = update.Value.CurrentState[ResolvedCompilationReference.SchemaName].Items;
foreach (var reference in references)
{
mvcReferenceFullPath = reference.Key;
break;
if (reference.Key.EndsWith(MvcAssemblyFileName, StringComparison.OrdinalIgnoreCase))
{
mvcReferenceFullPath = reference.Key;
break;
}
}
}
if (mvcReferenceFullPath == null)
{
// Ok we can't find an MVC version. Let's assume this project isn't using Razor then.
await UpdateProjectUnsafeAsync(null).ConfigureAwait(false);
await UpdateAsync(UninitializeProjectUnsafe).ConfigureAwait(false);
return;
}
@ -106,13 +109,16 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
if (version == null)
{
// Ok we can't find an MVC version. Let's assume this project isn't using Razor then.
await UpdateProjectUnsafeAsync(null).ConfigureAwait(false);
await UpdateAsync(UninitializeProjectUnsafe).ConfigureAwait(false);
return;
}
var configuration = FallbackRazorConfiguration.SelectConfiguration(version);
var hostProject = new HostProject(CommonServices.UnconfiguredProject.FullPath, configuration);
await UpdateProjectUnsafeAsync(hostProject).ConfigureAwait(false);
await UpdateAsync(() =>
{
UpdateProjectUnsafe(hostProject);
}).ConfigureAwait(false);
});
}, registerFaultHandler: true);
}

View File

@ -2,7 +2,10 @@
// 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.ComponentModel.Composition;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.VisualStudio.LanguageServices;
@ -18,6 +21,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
private ProjectSnapshotManagerBase _projectManager;
private HostProject _current;
private Dictionary<string, HostDocument> _currentDocuments;
public RazorProjectHostBase(
IUnconfiguredProjectCommonServices commonServices,
@ -38,6 +42,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
_workspace = workspace;
_lock = new AsyncSemaphore(initialCount: 1);
_currentDocuments = new Dictionary<string, HostDocument>(FilePathComparer.Instance);
}
// Internal for testing
@ -67,8 +72,11 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
_projectManager = projectManager;
_lock = new AsyncSemaphore(initialCount: 1);
_currentDocuments = new Dictionary<string, HostDocument>(FilePathComparer.Instance);
}
protected HostProject Current => _current;
protected IUnconfiguredProjectCommonServices CommonServices { get; }
// internal for tests. The product will call through the IProjectDynamicLoadComponent interface.
@ -94,7 +102,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
{
if (_current != null)
{
await UpdateProjectUnsafeAsync(null).ConfigureAwait(false);
await UpdateAsync(UninitializeProjectUnsafe).ConfigureAwait(false);
}
});
}
@ -113,10 +121,21 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
if (_current != null)
{
var old = _current;
await UpdateProjectUnsafeAsync(null).ConfigureAwait(false);
var oldDocuments = _currentDocuments.Values.ToArray();
var filePath = CommonServices.UnconfiguredProject.FullPath;
await UpdateProjectUnsafeAsync(new HostProject(filePath, old.Configuration)).ConfigureAwait(false);
await UpdateAsync(UninitializeProjectUnsafe).ConfigureAwait(false);
await UpdateAsync(() =>
{
var filePath = CommonServices.UnconfiguredProject.FullPath;
UpdateProjectUnsafe(new HostProject(filePath, old.Configuration));
// This should no-op in the common case, just putting it here for insurance.
for (var i = 0; i < oldDocuments.Length; i++)
{
AddDocumentUnsafe(oldDocuments[i]);
}
}).ConfigureAwait(false);
}
});
}
@ -134,12 +153,21 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
return _projectManager;
}
// Must be called inside the lock.
protected async Task UpdateProjectUnsafeAsync(HostProject project)
protected async Task UpdateAsync(Action action)
{
await CommonServices.ThreadingService.SwitchToUIThread();
var projectManager = GetProjectManager();
action();
}
protected void UninitializeProjectUnsafe()
{
ClearDocumentsUnsafe();
UpdateProjectUnsafe(null);
}
protected void UpdateProjectUnsafe(HostProject project)
{
var projectManager = GetProjectManager();
if (_current == null && project == null)
{
// This is a no-op. This project isn't using Razor.
@ -150,6 +178,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
}
else if (_current != null && project == null)
{
Debug.Assert(_currentDocuments.Count == 0);
projectManager.HostProjectRemoved(_current);
}
else
@ -159,6 +188,40 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
_current = project;
}
protected void AddDocumentUnsafe(HostDocument document)
{
var projectManager = GetProjectManager();
if (_currentDocuments.ContainsKey(document.FilePath))
{
// Ignore duplicates
return;
}
projectManager.DocumentAdded(_current, document);
_currentDocuments.Add(document.FilePath, document);
}
protected void RemoveDocumentUnsafe(HostDocument document)
{
var projectManager = GetProjectManager();
projectManager.DocumentRemoved(_current, document);
_currentDocuments.Remove(document.FilePath);
}
protected void ClearDocumentsUnsafe()
{
var projectManager = GetProjectManager();
foreach (var kvp in _currentDocuments)
{
_projectManager.DocumentRemoved(_current, kvp.Value);
}
_currentDocuments.Clear();
}
protected async Task ExecuteWithLock(Func<Task> func)
{

View File

@ -1,29 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Rule
Description="Configuration Properties"
DisplayName="Configuration Properties"
Name="RazorConfiguration"
PageTemplate="generic"
xmlns="http://schemas.microsoft.com/build/2009/properties">
<Rule.DataSource>
<DataSource
Persistence="ProjectFile"
HasConfigurationCondition="True"
ItemType="RazorConfiguration" />
</Rule.DataSource>
<Rule.Categories>
<Category
Name="General"
DisplayName="General" />
</Rule.Categories>
<StringProperty
Category="General"
Description="Razor Extensions"
DisplayName="Razor Extensions"
Name="Extensions"
ReadOnly="True"
Visible="True" />
</Rule>

View File

@ -1,37 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Rule
Description="Extension Properties"
DisplayName="Extension Properties"
Name="RazorExtension"
PageTemplate="generic"
xmlns="http://schemas.microsoft.com/build/2009/properties">
<Rule.DataSource>
<DataSource
Persistence="ProjectFile"
HasConfigurationCondition="True"
ItemType="RazorExtension" />
</Rule.DataSource>
<Rule.Categories>
<Category
Name="General"
DisplayName="General" />
</Rule.Categories>
<StringProperty
Category="General"
Description="Razor Extension Assembly Name"
DisplayName="Razor Extension Assembly Name"
Name="AssemblyName"
ReadOnly="True"
Visible="True" />
<StringProperty
Category="General"
Description="Razor Extension Assembly File Path"
DisplayName="Razor Extension Assembly File Path"
Name="AssemblyFilePath"
ReadOnly="True"
Visible="True" />
</Rule>

View File

@ -1,36 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Rule
Description="Razor Properties"
DisplayName="Razor Properties"
Name="RazorGeneral"
PageTemplate="generic"
xmlns="http://schemas.microsoft.com/build/2009/properties">
<Rule.DataSource>
<DataSource
Persistence="ProjectFile"
HasConfigurationCondition="True" />
</Rule.DataSource>
<Rule.Categories>
<Category
Name="General"
DisplayName="General" />
</Rule.Categories>
<StringProperty
Category="General"
Description="Razor Language Version"
DisplayName="Razor Language Version"
Name="RazorLangVersion"
ReadOnly="True"
Visible="True" />
<StringProperty
Category="General"
Description="Razor Configuration Name"
DisplayName="Razor Configuration Name"
Name="RazorDefaultConfiguration"
ReadOnly="True"
Visible="True" />
</Rule>

View File

@ -0,0 +1,212 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace Microsoft.CodeAnalysis.Razor.ProjectSystem.Rules {
internal partial class RazorGenerateWithTargetPath {
/// <summary>Backing field for deserialized rule.<see cref='Microsoft.Build.Framework.XamlTypes.Rule'/>.</summary>
private static Microsoft.Build.Framework.XamlTypes.Rule deserializedFallbackRule;
/// <summary>The name of the schema to look for at runtime to fulfill property access.</summary>
internal const string SchemaName = "RazorGenerateWithTargetPath";
/// <summary>The ItemType given in the Rule.DataSource property. May not apply to every Property's individual DataSource.</summary>
internal const string PrimaryDataSourceItemType = "RazorGenerateWithTargetPath";
/// <summary>The Label given in the Rule.DataSource property. May not apply to every Property's individual DataSource.</summary>
internal const string PrimaryDataSourceLabel = "";
/// <summary>Target Path (The "TargetPath" property).</summary>
internal const string TargetPathProperty = "TargetPath";
/// <summary>Backing field for the <see cref='Microsoft.Build.Framework.XamlTypes.Rule'/> property.</summary>
private Microsoft.VisualStudio.ProjectSystem.Properties.IRule rule;
/// <summary>Backing field for the file name of the rule property.</summary>
private string file;
/// <summary>Backing field for the ItemType property.</summary>
private string itemType;
/// <summary>Backing field for the ItemName property.</summary>
private string itemName;
/// <summary>Configured Project</summary>
private Microsoft.VisualStudio.ProjectSystem.ConfiguredProject configuredProject;
/// <summary>The dictionary of named catalogs.</summary>
private System.Collections.Immutable.IImmutableDictionary<string, Microsoft.VisualStudio.ProjectSystem.Properties.IPropertyPagesCatalog> catalogs;
/// <summary>Backing field for the <see cref='Microsoft.VisualStudio.ProjectSystem.Properties.IRule'/> property.</summary>
private Microsoft.VisualStudio.ProjectSystem.Properties.IRule fallbackRule;
/// <summary>Thread locking object</summary>
private object locker = new object();
/// <summary>Initializes a new instance of the RazorGenerateWithTargetPath class.</summary>
internal RazorGenerateWithTargetPath(Microsoft.VisualStudio.ProjectSystem.Properties.IRule rule) {
this.rule = rule;
}
/// <summary>Initializes a new instance of the RazorGenerateWithTargetPath class.</summary>
internal RazorGenerateWithTargetPath(Microsoft.VisualStudio.ProjectSystem.ConfiguredProject configuredProject, System.Collections.Immutable.IImmutableDictionary<string, Microsoft.VisualStudio.ProjectSystem.Properties.IPropertyPagesCatalog> catalogs, string context, string file, string itemType, string itemName) :
this(GetRule(System.Collections.Immutable.ImmutableDictionary.GetValueOrDefault(catalogs, context), file, itemType, itemName)) {
if ((configuredProject == null)) {
throw new System.ArgumentNullException("configuredProject");
}
this.configuredProject = configuredProject;
this.catalogs = catalogs;
this.file = file;
this.itemType = itemType;
this.itemName = itemName;
}
/// <summary>Initializes a new instance of the RazorGenerateWithTargetPath class.</summary>
internal RazorGenerateWithTargetPath(Microsoft.VisualStudio.ProjectSystem.Properties.IRule rule, Microsoft.VisualStudio.ProjectSystem.ConfiguredProject configuredProject) :
this(rule) {
if ((rule == null)) {
throw new System.ArgumentNullException("rule");
}
if ((configuredProject == null)) {
throw new System.ArgumentNullException("configuredProject");
}
this.configuredProject = configuredProject;
this.rule = rule;
this.file = this.rule.File;
this.itemType = this.rule.ItemType;
this.itemName = this.rule.ItemName;
}
/// <summary>Initializes a new instance of the RazorGenerateWithTargetPath class.</summary>
internal RazorGenerateWithTargetPath(Microsoft.VisualStudio.ProjectSystem.ConfiguredProject configuredProject, System.Collections.Immutable.IImmutableDictionary<string, Microsoft.VisualStudio.ProjectSystem.Properties.IPropertyPagesCatalog> catalogs, string context, Microsoft.VisualStudio.ProjectSystem.Properties.IProjectPropertiesContext propertyContext) :
this(configuredProject, catalogs, context, GetContextFile(propertyContext), propertyContext.ItemType, propertyContext.ItemName) {
}
/// <summary>Initializes a new instance of the RazorGenerateWithTargetPath class that assumes a project context (neither property sheet nor items).</summary>
internal RazorGenerateWithTargetPath(Microsoft.VisualStudio.ProjectSystem.ConfiguredProject configuredProject, System.Collections.Immutable.IImmutableDictionary<string, Microsoft.VisualStudio.ProjectSystem.Properties.IPropertyPagesCatalog> catalogs) :
this(configuredProject, catalogs, "Project", null, null, null) {
}
/// <summary>Gets the IRule used to get and set properties.</summary>
public Microsoft.VisualStudio.ProjectSystem.Properties.IRule Rule {
get {
return this.rule;
}
}
/// <summary>Target Path</summary>
internal Microsoft.VisualStudio.ProjectSystem.Properties.IEvaluatedProperty TargetPath {
get {
Microsoft.VisualStudio.ProjectSystem.Properties.IRule localRule = this.rule;
if ((localRule == null)) {
localRule = this.GeneratedFallbackRule;
}
if ((localRule == null)) {
return null;
}
Microsoft.VisualStudio.ProjectSystem.Properties.IEvaluatedProperty property = ((Microsoft.VisualStudio.ProjectSystem.Properties.IEvaluatedProperty)(localRule.GetProperty(TargetPathProperty)));
if (((property == null)
&& (this.GeneratedFallbackRule != null))) {
localRule = this.GeneratedFallbackRule;
property = ((Microsoft.VisualStudio.ProjectSystem.Properties.IEvaluatedProperty)(localRule.GetProperty(TargetPathProperty)));
}
return property;
}
}
/// <summary>Get the fallback rule if the current rule on disk is missing or a property in the rule on disk is missing</summary>
private Microsoft.VisualStudio.ProjectSystem.Properties.IRule GeneratedFallbackRule {
get {
if (((this.fallbackRule == null)
&& (this.configuredProject != null))) {
System.Threading.Monitor.Enter(this.locker);
try {
if ((this.fallbackRule == null)) {
this.InitializeFallbackRule();
}
}
finally {
System.Threading.Monitor.Exit(this.locker);
}
}
return this.fallbackRule;
}
}
private static Microsoft.VisualStudio.ProjectSystem.Properties.IRule GetRule(Microsoft.VisualStudio.ProjectSystem.Properties.IPropertyPagesCatalog catalog, string file, string itemType, string itemName) {
if ((catalog == null)) {
return null;
}
return catalog.BindToContext(SchemaName, file, itemType, itemName);
}
private static string GetContextFile(Microsoft.VisualStudio.ProjectSystem.Properties.IProjectPropertiesContext propertiesContext) {
if ((propertiesContext.IsProjectFile == true)) {
return null;
}
else {
return propertiesContext.File;
}
}
private void InitializeFallbackRule() {
if ((this.configuredProject == null)) {
return;
}
Microsoft.Build.Framework.XamlTypes.Rule unboundRule = RazorGenerateWithTargetPath.deserializedFallbackRule;
if ((unboundRule == null)) {
System.IO.Stream xamlStream = null;
System.Reflection.Assembly thisAssembly = System.Reflection.Assembly.GetExecutingAssembly();
try {
xamlStream = thisAssembly.GetManifestResourceStream("XamlRuleToCode:RazorGenerateWithTargetPath.xaml");
Microsoft.Build.Framework.XamlTypes.IProjectSchemaNode root = ((Microsoft.Build.Framework.XamlTypes.IProjectSchemaNode)(System.Xaml.XamlServices.Load(xamlStream)));
System.Collections.Generic.IEnumerator<System.Object> ruleEnumerator = root.GetSchemaObjects(typeof(Microsoft.Build.Framework.XamlTypes.Rule)).GetEnumerator();
for (
; ((unboundRule == null)
&& ruleEnumerator.MoveNext());
) {
Microsoft.Build.Framework.XamlTypes.Rule t = ((Microsoft.Build.Framework.XamlTypes.Rule)(ruleEnumerator.Current));
if (System.StringComparer.OrdinalIgnoreCase.Equals(t.Name, SchemaName)) {
unboundRule = t;
unboundRule.Name = "4d01f23e-96db-4c90-9c01-17b7f8b61243";
RazorGenerateWithTargetPath.deserializedFallbackRule = unboundRule;
}
}
}
finally {
if ((xamlStream != null)) {
((System.IDisposable)(xamlStream)).Dispose();
}
}
}
this.configuredProject.Services.AdditionalRuleDefinitions.AddRuleDefinition(unboundRule, "FallbackRuleCodeGenerationContext");
Microsoft.VisualStudio.ProjectSystem.Properties.IPropertyPagesCatalog catalog = this.configuredProject.Services.PropertyPagesCatalog.GetMemoryOnlyCatalog("FallbackRuleCodeGenerationContext");
this.fallbackRule = catalog.BindToContext(unboundRule.Name, this.file, this.itemType, this.itemName);
}
}
internal partial class RazorProjectProperties {
private static System.Func<System.Threading.Tasks.Task<System.Collections.Immutable.IImmutableDictionary<string, Microsoft.VisualStudio.ProjectSystem.Properties.IPropertyPagesCatalog>>, object, RazorGenerateWithTargetPath> CreateRazorGenerateWithTargetPathPropertiesDelegate = new System.Func<System.Threading.Tasks.Task<System.Collections.Immutable.IImmutableDictionary<string, Microsoft.VisualStudio.ProjectSystem.Properties.IPropertyPagesCatalog>>, object, RazorGenerateWithTargetPath>(CreateRazorGenerateWithTargetPathProperties);
private static RazorGenerateWithTargetPath CreateRazorGenerateWithTargetPathProperties(System.Threading.Tasks.Task<System.Collections.Immutable.IImmutableDictionary<string, Microsoft.VisualStudio.ProjectSystem.Properties.IPropertyPagesCatalog>> namedCatalogs, object state) {
RazorProjectProperties that = ((RazorProjectProperties)(state));
return new RazorGenerateWithTargetPath(that.ConfiguredProject, namedCatalogs.Result, "Project", that.File, that.ItemType, that.ItemName);
}
/// <summary>Gets the strongly-typed property accessor used to get and set Razor Document Properties properties.</summary>
internal System.Threading.Tasks.Task<RazorGenerateWithTargetPath> GetRazorGenerateWithTargetPathPropertiesAsync() {
System.Threading.Tasks.Task<System.Collections.Immutable.IImmutableDictionary<string, Microsoft.VisualStudio.ProjectSystem.Properties.IPropertyPagesCatalog>> namedCatalogsTask = this.GetNamedCatalogsAsync();
return namedCatalogsTask.ContinueWith(CreateRazorGenerateWithTargetPathPropertiesDelegate, this, System.Threading.CancellationToken.None, System.Threading.Tasks.TaskContinuationOptions.ExecuteSynchronously, System.Threading.Tasks.TaskScheduler.Default);
}
}
}

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.
#if RAZOR_EXTENSION_DEVELOPER_MODE
namespace Microsoft.VisualStudio.LanguageServices.Razor
{
internal class RazorEngineDocument
{
public string Text { get; set; }
}
}
#endif

View File

@ -86,14 +86,15 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor
public int UpdateProjectCfg_Done(IVsHierarchy pHierProj, IVsCfg pCfgProj, IVsCfg pCfgSln, uint dwAction, int fSuccess, int fCancel)
{
var projectPath = _projectService.GetProjectPath(pHierProj);
// Get the corresponding roslyn project by matching the project name and the project path.
foreach (var projectSnapshot in _projectManager.Projects)
var project = _projectManager.GetLoadedProject(projectPath);
if (project != null && project.WorkspaceProject != null)
{
if (string.Equals(projectPath, projectSnapshot.FilePath, StringComparison.OrdinalIgnoreCase))
var workspaceProject = _projectManager.Workspace.CurrentSolution.GetProject(project.WorkspaceProject.Id);
if (workspaceProject != null)
{
_projectManager.HostProjectBuildComplete(projectSnapshot.HostProject);
break;
// Trigger a tag helper update by forcing the project manager to see the workspace Project
// from the current solution.
_projectManager.WorkspaceProjectChanged(workspaceProject);
}
}

View File

@ -97,14 +97,15 @@ namespace Microsoft.VisualStudio.Mac.LanguageServices.Razor
}
var projectPath = _projectService.GetProjectPath(projectItem);
// Get the corresponding roslyn project by matching the project name and the project path.
foreach (var projectSnapshot in _projectManager.Projects)
var project = _projectManager.GetLoadedProject(projectPath);
if (project != null && project.WorkspaceProject != null)
{
if (string.Equals(projectPath, projectSnapshot.FilePath, StringComparison.OrdinalIgnoreCase))
var workspaceProject = _projectManager.Workspace.CurrentSolution.GetProject(project.WorkspaceProject.Id);
if (workspaceProject != null)
{
_projectManager.HostProjectBuildComplete(projectSnapshot.HostProject);
break;
// Trigger a tag helper update by forcing the project manager to see the workspace Project
// from the current solution.
_projectManager.WorkspaceProjectChanged(workspaceProject);
}
}
}

View File

@ -8,12 +8,12 @@ using Microsoft.CodeAnalysis.Razor;
namespace Microsoft.CodeAnalysis.Host
{
internal class TestRazorLanguageServices : HostLanguageServices
internal class TestLanguageServices : HostLanguageServices
{
private readonly HostWorkspaceServices _workspaceServices;
private readonly IEnumerable<ILanguageService> _languageServices;
public TestRazorLanguageServices(HostWorkspaceServices workspaceServices, IEnumerable<ILanguageService> languageServices)
public TestLanguageServices(HostWorkspaceServices workspaceServices, IEnumerable<ILanguageService> languageServices)
{
if (workspaceServices == null)
{

View File

@ -13,14 +13,14 @@ namespace Microsoft.CodeAnalysis.Host
private static readonly Workspace DefaultWorkspace = TestWorkspace.Create();
private readonly HostServices _hostServices;
private readonly HostLanguageServices _razorLanguageServices;
private readonly IEnumerable<IWorkspaceService> _workspaceServices;
private readonly TestRazorLanguageServices _razorLanguageServices;
private readonly Workspace _workspace;
public TestWorkspaceServices(
HostServices hostServices,
IEnumerable<IWorkspaceService> workspaceServices,
IEnumerable<ILanguageService> razorLanguageServices,
IEnumerable<ILanguageService> languageServices,
Workspace workspace)
{
if (hostServices == null)
@ -33,9 +33,9 @@ namespace Microsoft.CodeAnalysis.Host
throw new ArgumentNullException(nameof(workspaceServices));
}
if (razorLanguageServices == null)
if (languageServices == null)
{
throw new ArgumentNullException(nameof(razorLanguageServices));
throw new ArgumentNullException(nameof(languageServices));
}
if (workspace == null)
@ -45,8 +45,9 @@ namespace Microsoft.CodeAnalysis.Host
_hostServices = hostServices;
_workspaceServices = workspaceServices;
_razorLanguageServices = new TestRazorLanguageServices(this, razorLanguageServices);
_workspace = workspace;
_razorLanguageServices = new TestLanguageServices(this, languageServices);
}
public override HostServices HostServices => _hostServices;
@ -68,12 +69,13 @@ namespace Microsoft.CodeAnalysis.Host
public override HostLanguageServices GetLanguageServices(string languageName)
{
if (languageName != RazorLanguage.Name)
if (languageName == RazorLanguage.Name)
{
throw new InvalidOperationException($"Test services do not support language service '{languageName}'. The only language services supported are '{RazorLanguage.Name}'.");
return _razorLanguageServices;
}
return _razorLanguageServices;
// Fallback to default host services to resolve roslyn specific features.
return DefaultWorkspace.Services.GetLanguageServices(languageName);
}
public override IEnumerable<string> SupportedLanguages => new[] { RazorLanguage.Name };

View File

@ -2,108 +2,119 @@
// 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.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.CodeAnalysis.Host;
using Xunit;
namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
{
public class DefaultProjectSnapshotTest
{
[Fact]
public void WithWorkspaceProject_CreatesSnapshot_UpdatesUnderlyingProject()
public DefaultProjectSnapshotTest()
{
// Arrange
var hostProject = new HostProject("Test.cshtml", FallbackRazorConfiguration.MVC_2_0);
var workspaceProject = GetWorkspaceProject("Test1");
var original = new DefaultProjectSnapshot(hostProject, workspaceProject);
TagHelperResolver = new TestTagHelperResolver();
var anotherProject = GetWorkspaceProject("Test1");
// Act
var snapshot = original.WithWorkspaceProject(anotherProject);
// Assert
Assert.Same(anotherProject, snapshot.WorkspaceProject);
Assert.Equal(original.ComputedVersion, snapshot.ComputedVersion);
Assert.Equal(original.Configuration, snapshot.Configuration);
Assert.Equal(original.TagHelpers, snapshot.TagHelpers);
}
[Fact]
public void WithProjectChange_WithProject_CreatesSnapshot_UpdatesValues()
{
// Arrange
var hostProject = new HostProject("Test.cshtml", FallbackRazorConfiguration.MVC_2_0);
var workspaceProject = GetWorkspaceProject("Test1");
var original = new DefaultProjectSnapshot(hostProject, workspaceProject);
var anotherProject = GetWorkspaceProject("Test1");
var update = new ProjectSnapshotUpdateContext(original.FilePath, hostProject, anotherProject, original.Version)
{
TagHelpers = Array.Empty<TagHelperDescriptor>(),
};
// Act
var snapshot = original.WithComputedUpdate(update);
// Assert
Assert.Same(original.WorkspaceProject, snapshot.WorkspaceProject);
Assert.Same(update.TagHelpers, snapshot.TagHelpers);
}
[Fact]
public void HaveTagHelpersChanged_NoUpdatesToTagHelpers_ReturnsFalse()
{
// Arrange
var hostProject = new HostProject("Test1.csproj", RazorConfiguration.Default);
var workspaceProject = GetWorkspaceProject("Test1");
var original = new DefaultProjectSnapshot(hostProject, workspaceProject);
var anotherProject = GetWorkspaceProject("Test1");
var update = new ProjectSnapshotUpdateContext("Test1.csproj", hostProject, anotherProject, VersionStamp.Default);
var snapshot = original.WithComputedUpdate(update);
// Act
var result = snapshot.HaveTagHelpersChanged(original);
// Assert
Assert.False(result);
}
[Fact]
public void HaveTagHelpersChanged_TagHelpersUpdated_ReturnsTrue()
{
// Arrange
var hostProject = new HostProject("Test1.csproj", RazorConfiguration.Default);
var workspaceProject = GetWorkspaceProject("Test1");
var original = new DefaultProjectSnapshot(hostProject, workspaceProject);
var anotherProject = GetWorkspaceProject("Test1");
var update = new ProjectSnapshotUpdateContext("Test1.csproj", hostProject, anotherProject, VersionStamp.Default)
{
TagHelpers = new[]
HostServices = TestServices.Create(
new IWorkspaceService[]
{
TagHelperDescriptorBuilder.Create("One", "TestAssembly").Build(),
TagHelperDescriptorBuilder.Create("Two", "TestAssembly").Build(),
new TestProjectSnapshotProjectEngineFactory(),
},
new ILanguageService[]
{
TagHelperResolver,
});
HostProject = new HostProject("c:\\MyProject\\Test.csproj", FallbackRazorConfiguration.MVC_2_0);
HostProjectWithConfigurationChange = new HostProject("c:\\MyProject\\Test.csproj", FallbackRazorConfiguration.MVC_1_0);
Workspace = TestWorkspace.Create(HostServices);
var projectId = ProjectId.CreateNewId("Test");
var solution = Workspace.CurrentSolution.AddProject(ProjectInfo.Create(
projectId,
VersionStamp.Default,
"Test",
"Test",
LanguageNames.CSharp,
"c:\\MyProject\\Test.csproj"));
WorkspaceProject = solution.GetProject(projectId);
SomeTagHelpers = new List<TagHelperDescriptor>();
SomeTagHelpers.Add(TagHelperDescriptorBuilder.Create("Test1", "TestAssembly").Build());
Documents = new HostDocument[]
{
new HostDocument("c:\\MyProject\\File.cshtml", "File.cshtml"),
new HostDocument("c:\\MyProject\\Index.cshtml", "Index.cshtml"),
// linked file
new HostDocument("c:\\SomeOtherProject\\Index.cshtml", "Pages\\Index.cshtml"),
};
var snapshot = original.WithComputedUpdate(update);
// Act
var result = snapshot.HaveTagHelpersChanged(original);
// Assert
Assert.True(result);
}
private Project GetWorkspaceProject(string name)
private HostDocument[] Documents { get; }
private HostProject HostProject { get; }
private HostProject HostProjectWithConfigurationChange { get; }
private Project WorkspaceProject { get; }
private TestTagHelperResolver TagHelperResolver { get; }
private HostServices HostServices { get; }
private Workspace Workspace { get; }
private List<TagHelperDescriptor> SomeTagHelpers { get; }
[Fact]
public void ProjectSnapshot_CachesDocumentSnapshots()
{
Project project = null;
TestWorkspace.Create(workspace =>
// Arrange
var state = new ProjectState(Workspace.Services, HostProject, WorkspaceProject)
.AddHostDocument(Documents[0])
.AddHostDocument(Documents[1])
.AddHostDocument(Documents[2]);
var snapshot = new DefaultProjectSnapshot(state);
// Act
var documents = snapshot.DocumentFilePaths.ToDictionary(f => f, f => snapshot.GetDocument(f));
// Assert
Assert.Collection(
documents,
d => Assert.Same(d.Value, snapshot.GetDocument(d.Key)),
d => Assert.Same(d.Value, snapshot.GetDocument(d.Key)),
d => Assert.Same(d.Value, snapshot.GetDocument(d.Key)));
}
[Fact]
public void ProjectSnapshot_CachesTagHelperTask()
{
// Arrange
TagHelperResolver.CompletionSource = new TaskCompletionSource<TagHelperResolutionResult>();
try
{
project = workspace.AddProject(name, LanguageNames.CSharp);
});
return project;
var state = new ProjectState(Workspace.Services, HostProject, WorkspaceProject);
var snapshot = new DefaultProjectSnapshot(state);
// Act
var task1 = snapshot.GetTagHelpersAsync();
var task2 = snapshot.GetTagHelpersAsync();
// Assert
Assert.Same(task1, task2);
}
finally
{
TagHelperResolver.CompletionSource.SetCanceled();
}
}
}
}

View File

@ -0,0 +1,93 @@
// 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.Linq;
using System.Text;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.CodeAnalysis.Host;
using Moq;
using Xunit;
namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
{
public class DocumentStateTest
{
public DocumentStateTest()
{
TagHelperResolver = new TestTagHelperResolver();
HostServices = TestServices.Create(
new IWorkspaceService[]
{
new TestProjectSnapshotProjectEngineFactory(),
},
new ILanguageService[]
{
TagHelperResolver,
});
HostProject = new HostProject("c:\\MyProject\\Test.csproj", FallbackRazorConfiguration.MVC_2_0);
HostProjectWithConfigurationChange = new HostProject("c:\\MyProject\\Test.csproj", FallbackRazorConfiguration.MVC_1_0);
Workspace = TestWorkspace.Create(HostServices);
var projectId = ProjectId.CreateNewId("Test");
var solution = Workspace.CurrentSolution.AddProject(ProjectInfo.Create(
projectId,
VersionStamp.Default,
"Test",
"Test",
LanguageNames.CSharp,
"c:\\MyProject\\Test.csproj"));
WorkspaceProject = solution.GetProject(projectId);
SomeTagHelpers = new List<TagHelperDescriptor>();
SomeTagHelpers.Add(TagHelperDescriptorBuilder.Create("Test1", "TestAssembly").Build());
Document = new HostDocument("c:\\MyProject\\File.cshtml", "File.cshtml");
}
private HostDocument Document { get; }
private HostProject HostProject { get; }
private HostProject HostProjectWithConfigurationChange { get; }
private Project WorkspaceProject { get; }
private TestTagHelperResolver TagHelperResolver { get; }
private HostServices HostServices { get; }
private Workspace Workspace { get; }
private List<TagHelperDescriptor> SomeTagHelpers { get; }
[Fact]
public void DocumentState_ConstructedNew()
{
// Arrange
// Act
var state = new DocumentState(Workspace.Services, Document);
// Assert
Assert.NotEqual(VersionStamp.Default, state.Version);
}
[Fact] // There's no magic in the constructor.
public void ProjectState_ConstructedFromCopy()
{
// Arrange
var original = new DocumentState(Workspace.Services, Document);
// Act
var state = new DocumentState(original, ProjectDifference.ConfigurationChanged);
// Assert
Assert.NotEqual(original.Version, state.Version);
}
}
}

View File

@ -0,0 +1,359 @@
// 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.Linq;
using System.Text;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.CodeAnalysis.Host;
using Moq;
using Xunit;
namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
{
public class ProjectStateTest
{
public ProjectStateTest()
{
TagHelperResolver = new TestTagHelperResolver();
HostServices = TestServices.Create(
new IWorkspaceService[]
{
new TestProjectSnapshotProjectEngineFactory(),
},
new ILanguageService[]
{
TagHelperResolver,
});
HostProject = new HostProject("c:\\MyProject\\Test.csproj", FallbackRazorConfiguration.MVC_2_0);
HostProjectWithConfigurationChange = new HostProject("c:\\MyProject\\Test.csproj", FallbackRazorConfiguration.MVC_1_0);
Workspace = TestWorkspace.Create(HostServices);
var projectId = ProjectId.CreateNewId("Test");
var solution = Workspace.CurrentSolution.AddProject(ProjectInfo.Create(
projectId,
VersionStamp.Default,
"Test",
"Test",
LanguageNames.CSharp,
"c:\\MyProject\\Test.csproj"));
WorkspaceProject = solution.GetProject(projectId);
SomeTagHelpers = new List<TagHelperDescriptor>();
SomeTagHelpers.Add(TagHelperDescriptorBuilder.Create("Test1", "TestAssembly").Build());
Documents = new HostDocument[]
{
new HostDocument("c:\\MyProject\\File.cshtml", "File.cshtml"),
new HostDocument("c:\\MyProject\\Index.cshtml", "Index.cshtml"),
// linked file
new HostDocument("c:\\SomeOtherProject\\Index.cshtml", "Pages\\Index.cshtml"),
};
}
private HostDocument[] Documents { get; }
private HostProject HostProject { get; }
private HostProject HostProjectWithConfigurationChange { get; }
private Project WorkspaceProject { get; }
private TestTagHelperResolver TagHelperResolver { get; }
private HostServices HostServices { get; }
private Workspace Workspace { get; }
private List<TagHelperDescriptor> SomeTagHelpers { get; }
[Fact]
public void ProjectState_ConstructedNew()
{
// Arrange
// Act
var state = new ProjectState(Workspace.Services, HostProject, WorkspaceProject);
// Assert
Assert.Empty(state.Documents);
Assert.NotEqual(VersionStamp.Default, state.Version);
}
[Fact] // There's no magic in the constructor.
public void ProjectState_ConstructedFromCopy()
{
// Arrange
var original = new ProjectState(Workspace.Services, HostProject, WorkspaceProject);
// Act
var state = new ProjectState(original, ProjectDifference.None, HostProject, WorkspaceProject, original.Documents);
// Assert
Assert.Same(original.Documents, state.Documents);
Assert.NotEqual(original.Version, state.Version);
}
[Fact]
public void ProjectState_AddHostDocument_ToEmpty()
{
// Arrange
var original = new ProjectState(Workspace.Services, HostProject, WorkspaceProject);
// Act
var state = original.AddHostDocument(Documents[0]);
// Assert
Assert.NotEqual(original.Version, state.Version);
Assert.Collection(
state.Documents.OrderBy(kvp => kvp.Key),
d => Assert.Same(Documents[0], d.Value.HostDocument));
}
[Fact]
public void ProjectState_AddHostDocument_ToProjectWithDocuments()
{
// Arrange
var original = new ProjectState(Workspace.Services, HostProject, WorkspaceProject)
.AddHostDocument(Documents[2])
.AddHostDocument(Documents[1]);
// Act
var state = original.AddHostDocument(Documents[0]);
// Assert
Assert.NotEqual(original.Version, state.Version);
Assert.Collection(
state.Documents.OrderBy(kvp => kvp.Key),
d => Assert.Same(Documents[0], d.Value.HostDocument),
d => Assert.Same(Documents[1], d.Value.HostDocument),
d => Assert.Same(Documents[2], d.Value.HostDocument));
}
[Fact]
public void ProjectState_AddHostDocument_RetainsComputedState()
{
// Arrange
var original = new ProjectState(Workspace.Services, HostProject, WorkspaceProject)
.AddHostDocument(Documents[2])
.AddHostDocument(Documents[1]);
// Force init
GC.KeepAlive(original.ProjectEngine);
GC.KeepAlive(original.TagHelpers);
// Act
var state = original.AddHostDocument(Documents[0]);
// Assert
Assert.Same(original.ProjectEngine, state.ProjectEngine);
Assert.Same(original.TagHelpers, state.TagHelpers);
Assert.Same(original.Documents[Documents[1].FilePath], state.Documents[Documents[1].FilePath]);
Assert.Same(original.Documents[Documents[2].FilePath], state.Documents[Documents[2].FilePath]);
}
[Fact]
public void ProjectState_AddHostDocument_DuplicateNoops()
{
// Arrange
var original = new ProjectState(Workspace.Services, HostProject, WorkspaceProject)
.AddHostDocument(Documents[2])
.AddHostDocument(Documents[1]);
// Act
var state = original.AddHostDocument(new HostDocument(Documents[1].FilePath, "SomePath.cshtml"));
// Assert
Assert.Same(original, state);
}
[Fact]
public void ProjectState_RemoveHostDocument_FromProjectWithDocuments()
{
// Arrange
var original = new ProjectState(Workspace.Services, HostProject, WorkspaceProject)
.AddHostDocument(Documents[2])
.AddHostDocument(Documents[1]);
// Act
var state = original.RemoveHostDocument(Documents[1]);
// Assert
Assert.NotEqual(original.Version, state.Version);
Assert.Collection(
state.Documents.OrderBy(kvp => kvp.Key),
d => Assert.Same(Documents[2], d.Value.HostDocument));
}
[Fact]
public void ProjectState_RemoveHostDocument_RetainsComputedState()
{
// Arrange
var original = new ProjectState(Workspace.Services, HostProject, WorkspaceProject)
.AddHostDocument(Documents[2])
.AddHostDocument(Documents[1]);
// Force init
GC.KeepAlive(original.ProjectEngine);
GC.KeepAlive(original.TagHelpers);
// Act
var state = original.RemoveHostDocument(Documents[2]);
// Assert
Assert.Same(original.ProjectEngine, state.ProjectEngine);
Assert.Same(original.TagHelpers, state.TagHelpers);
Assert.Same(original.Documents[Documents[1].FilePath], state.Documents[Documents[1].FilePath]);
}
[Fact]
public void ProjectState_RemoveHostDocument_NotFoundNoops()
{
// Arrange
var original = new ProjectState(Workspace.Services, HostProject, WorkspaceProject)
.AddHostDocument(Documents[2])
.AddHostDocument(Documents[1]);
// Act
var state = original.RemoveHostDocument(Documents[0]);
// Assert
Assert.Same(original, state);
}
[Fact]
public void ProjectState_WithHostProject_ConfigurationChange_UpdatesComputedState()
{
// Arrange
var original = new ProjectState(Workspace.Services, HostProject, WorkspaceProject)
.AddHostDocument(Documents[2])
.AddHostDocument(Documents[1]);
// Force init
GC.KeepAlive(original.ProjectEngine);
GC.KeepAlive(original.TagHelpers);
// Act
var state = original.WithHostProject(HostProjectWithConfigurationChange);
// Assert
Assert.NotEqual(original.Version, state.Version);
Assert.Same(HostProjectWithConfigurationChange, state.HostProject);
Assert.NotSame(original.ProjectEngine, state.ProjectEngine);
Assert.NotSame(original.TagHelpers, state.TagHelpers);
Assert.NotSame(original.Documents[Documents[1].FilePath], state.Documents[Documents[1].FilePath]);
Assert.NotSame(original.Documents[Documents[1].FilePath], state.Documents[Documents[1].FilePath]);
}
[Fact]
public void ProjectState_WithHostProject_NoConfigurationChange_Noops()
{
// Arrange
var original = new ProjectState(Workspace.Services, HostProject, WorkspaceProject)
.AddHostDocument(Documents[2])
.AddHostDocument(Documents[1]);
// Force init
GC.KeepAlive(original.ProjectEngine);
GC.KeepAlive(original.TagHelpers);
// Act
var state = original.WithHostProject(HostProject);
// Assert
Assert.Same(original, state);
}
[Fact]
public void ProjectState_WithWorkspaceProject_Removed()
{
// Arrange
var original = new ProjectState(Workspace.Services, HostProject, WorkspaceProject)
.AddHostDocument(Documents[2])
.AddHostDocument(Documents[1]);
// Force init
GC.KeepAlive(original.ProjectEngine);
GC.KeepAlive(original.TagHelpers);
// Act
var state = original.WithWorkspaceProject(null);
// Assert
Assert.NotEqual(original.Version, state.Version);
Assert.Null(state.WorkspaceProject);
Assert.Same(original.ProjectEngine, state.ProjectEngine);
Assert.NotSame(original.TagHelpers, state.TagHelpers);
Assert.NotSame(original.Documents[Documents[1].FilePath], state.Documents[Documents[1].FilePath]);
Assert.NotSame(original.Documents[Documents[1].FilePath], state.Documents[Documents[1].FilePath]);
}
[Fact]
public void ProjectState_WithWorkspaceProject_Added()
{
// Arrange
var original = new ProjectState(Workspace.Services, HostProject, null)
.AddHostDocument(Documents[2])
.AddHostDocument(Documents[1]);
// Force init
GC.KeepAlive(original.ProjectEngine);
GC.KeepAlive(original.TagHelpers);
// Act
var state = original.WithWorkspaceProject(WorkspaceProject);
// Assert
Assert.NotEqual(original.Version, state.Version);
Assert.Same(WorkspaceProject, state.WorkspaceProject);
Assert.Same(original.ProjectEngine, state.ProjectEngine);
Assert.NotSame(original.TagHelpers, state.TagHelpers);
Assert.NotSame(original.Documents[Documents[1].FilePath], state.Documents[Documents[1].FilePath]);
Assert.NotSame(original.Documents[Documents[1].FilePath], state.Documents[Documents[1].FilePath]);
}
[Fact]
public void ProjectState_WithWorkspaceProject_Changed()
{
// Arrange
var original = new ProjectState(Workspace.Services, HostProject, WorkspaceProject)
.AddHostDocument(Documents[2])
.AddHostDocument(Documents[1]);
// Force init
GC.KeepAlive(original.ProjectEngine);
GC.KeepAlive(original.TagHelpers);
var changed = WorkspaceProject.WithAssemblyName("Test1");
// Act
var state = original.WithWorkspaceProject(changed);
// Assert
Assert.NotEqual(original.Version, state.Version);
Assert.Same(changed, state.WorkspaceProject);
Assert.Same(original.ProjectEngine, state.ProjectEngine);
Assert.NotSame(original.TagHelpers, state.TagHelpers);
Assert.NotSame(original.Documents[Documents[1].FilePath], state.Documents[Documents[1].FilePath]);
Assert.NotSame(original.Documents[Documents[1].FilePath], state.Documents[Documents[1].FilePath]);
}
}
}

View File

@ -3,8 +3,6 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Moq;
using Xunit;
@ -214,23 +212,9 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
private class TestProjectSnapshotManager : DefaultProjectSnapshotManager
{
public TestProjectSnapshotManager(IEnumerable<ProjectSnapshotChangeTrigger> triggers, Workspace workspace)
: base(Mock.Of<ForegroundDispatcher>(), Mock.Of<ErrorReporter>(), new TestProjectSnapshotWorker(), triggers, workspace)
: base(Mock.Of<ForegroundDispatcher>(), Mock.Of<ErrorReporter>(), triggers, workspace)
{
}
protected override void NotifyBackgroundWorker(ProjectSnapshotUpdateContext context)
{
Assert.NotNull(context.HostProject);
Assert.NotNull(context.WorkspaceProject);
}
}
private class TestProjectSnapshotWorker : ProjectSnapshotWorker
{
public override Task ProcessUpdateAsync(ProjectSnapshotUpdateContext update, CancellationToken cancellationToken = default(CancellationToken))
{
return Task.CompletedTask;
}
}
}
}

View File

@ -0,0 +1,41 @@
// 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.Linq;
using Moq;
namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
{
internal class TestProjectSnapshotManager : DefaultProjectSnapshotManager
{
public TestProjectSnapshotManager(Workspace workspace)
: base(Mock.Of<ForegroundDispatcher>(), Mock.Of<ErrorReporter>(), Enumerable.Empty<ProjectSnapshotChangeTrigger>(), workspace)
{
}
public TestProjectSnapshotManager(ForegroundDispatcher foregroundDispatcher, Workspace workspace)
: base(foregroundDispatcher, Mock.Of<ErrorReporter>(), Enumerable.Empty<ProjectSnapshotChangeTrigger>(), workspace)
{
}
public bool AllowNotifyListeners { get; set; }
public DefaultProjectSnapshot GetSnapshot(HostProject hostProject)
{
return Projects.Cast<DefaultProjectSnapshot>().FirstOrDefault(s => s.FilePath == hostProject.FilePath);
}
public DefaultProjectSnapshot GetSnapshot(Project workspaceProject)
{
return Projects.Cast<DefaultProjectSnapshot>().FirstOrDefault(s => s.FilePath == workspaceProject.FilePath);
}
protected override void NotifyListeners(ProjectChangeEventArgs e)
{
if (AllowNotifyListeners)
{
base.NotifyListeners(e);
}
}
}
}

View File

@ -0,0 +1,28 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.AspNetCore.Razor.Language;
namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
{
internal class TestProjectSnapshotProjectEngineFactory : ProjectSnapshotProjectEngineFactory
{
public RazorProjectEngine Engine { get; set; }
public override RazorProjectEngine Create(ProjectSnapshot project, RazorProjectFileSystem fileSystem, Action<RazorProjectEngineBuilder> configure)
{
return Engine ?? RazorProjectEngine.Create(project.Configuration, fileSystem, configure);
}
public override IProjectEngineFactory FindFactory(ProjectSnapshot project)
{
throw new NotImplementedException();
}
public override IProjectEngineFactory FindSerializableFactory(ProjectSnapshot project)
{
throw new NotImplementedException();
}
}
}

View File

@ -0,0 +1,32 @@
// 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.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
namespace Microsoft.CodeAnalysis.Razor
{
internal class TestTagHelperResolver : TagHelperResolver
{
public TaskCompletionSource<TagHelperResolutionResult> CompletionSource { get; set; }
public IList<TagHelperDescriptor> TagHelpers { get; } = new List<TagHelperDescriptor>();
public override Task<TagHelperResolutionResult> GetTagHelpersAsync(ProjectSnapshot project, CancellationToken cancellationToken = default)
{
if (CompletionSource == null)
{
return Task.FromResult(new TagHelperResolutionResult(TagHelpers.ToArray(), Array.Empty<RazorDiagnostic>()));
}
else
{
return CompletionSource.Task;
}
}
}
}

View File

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.IO;
using Microsoft.AspNetCore.Mvc.Razor.Extensions;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.CodeAnalysis.Razor;
@ -13,23 +14,51 @@ namespace Microsoft.VisualStudio.Editor.Razor
{
public class DefaultImportDocumentManagerIntegrationTest : ForegroundDispatcherTestBase
{
public DefaultImportDocumentManagerIntegrationTest()
{
ProjectPath = "C:\\path\\to\\project\\project.csproj";
FileSystem = RazorProjectFileSystem.Create(Path.GetDirectoryName(ProjectPath));
ProjectEngine = RazorProjectEngine.Create(FallbackRazorConfiguration.MVC_2_1, FileSystem, b =>
{
// These tests rely on MVC's import behavior.
Microsoft.AspNetCore.Mvc.Razor.Extensions.RazorExtensions.Register(b);
});
}
private string FilePath { get; }
private string ProjectPath { get; }
private RazorProjectFileSystem FileSystem { get; }
private RazorProjectEngine ProjectEngine { get; }
[ForegroundFact]
public void Changed_TrackerChanged_ResultsInChangedHavingCorrectArgs()
{
// Arrange
var filePath = "C:\\path\\to\\project\\Views\\Home\\file.cshtml";
var anotherFilePath = "C:\\path\\to\\project\\anotherFile.cshtml";
var projectPath = "C:\\path\\to\\project\\project.csproj";
var testImportsPath = "C:\\path\\to\\project\\_ViewImports.cshtml";
var tracker = Mock.Of<VisualStudioDocumentTracker>(t => t.FilePath == filePath && t.ProjectPath == projectPath);
var anotherTracker = Mock.Of<VisualStudioDocumentTracker>(t => t.FilePath == anotherFilePath && t.ProjectPath == projectPath);
var projectEngineFactoryService = GetProjectEngineFactoryService();
var fileChangeTracker = new Mock<FileChangeTracker>();
fileChangeTracker.Setup(f => f.FilePath).Returns(testImportsPath);
var tracker = Mock.Of<VisualStudioDocumentTracker>(
t => t.FilePath == "C:\\path\\to\\project\\Views\\Home\\file.cshtml" &&
t.ProjectPath == ProjectPath &&
t.ProjectSnapshot == Mock.Of<ProjectSnapshot>(p => p.GetProjectEngine() == ProjectEngine));
var anotherTracker = Mock.Of<VisualStudioDocumentTracker>(
t => t.FilePath == "C:\\path\\to\\project\\anotherFile.cshtml" &&
t.ProjectPath == ProjectPath &&
t.ProjectSnapshot == Mock.Of<ProjectSnapshot>(p => p.GetProjectEngine() == ProjectEngine));
var fileChangeTrackerFactory = new Mock<FileChangeTrackerFactory>();
var fileChangeTracker = new Mock<FileChangeTracker>();
fileChangeTracker
.Setup(f => f.FilePath)
.Returns(testImportsPath);
fileChangeTrackerFactory
.Setup(f => f.Create(testImportsPath))
.Returns(fileChangeTracker.Object);
fileChangeTrackerFactory
.Setup(f => f.Create("C:\\path\\to\\project\\Views\\_ViewImports.cshtml"))
.Returns(Mock.Of<FileChangeTracker>());
@ -38,7 +67,7 @@ namespace Microsoft.VisualStudio.Editor.Razor
.Returns(Mock.Of<FileChangeTracker>());
var called = false;
var manager = new DefaultImportDocumentManager(Dispatcher, new DefaultErrorReporter(), fileChangeTrackerFactory.Object, projectEngineFactoryService);
var manager = new DefaultImportDocumentManager(Dispatcher, new DefaultErrorReporter(), fileChangeTrackerFactory.Object);
manager.OnSubscribed(tracker);
manager.OnSubscribed(anotherTracker);
manager.Changed += (sender, args) =>
@ -49,8 +78,8 @@ namespace Microsoft.VisualStudio.Editor.Razor
Assert.Equal(FileChangeKind.Changed, args.Kind);
Assert.Collection(
args.AssociatedDocuments,
f => Assert.Equal(filePath, f),
f => Assert.Equal(anotherFilePath, f));
f => Assert.Equal("C:\\path\\to\\project\\Views\\Home\\file.cshtml", f),
f => Assert.Equal("C:\\path\\to\\project\\anotherFile.cshtml", f));
};
// Act
@ -59,25 +88,5 @@ namespace Microsoft.VisualStudio.Editor.Razor
// Assert
Assert.True(called);
}
private RazorProjectEngineFactoryService GetProjectEngineFactoryService()
{
var projectManager = new Mock<ProjectSnapshotManager>();
projectManager.Setup(p => p.Projects).Returns(Array.Empty<ProjectSnapshot>());
var projectEngineFactory = new Mock<IFallbackProjectEngineFactory>();
projectEngineFactory.Setup(s => s.Create(It.IsAny<RazorConfiguration>(), It.IsAny<RazorProjectFileSystem>(), It.IsAny<Action<RazorProjectEngineBuilder>>()))
.Returns<RazorConfiguration, RazorProjectFileSystem, Action<RazorProjectEngineBuilder>>(
(c, fs, b) => RazorProjectEngine.Create(
RazorConfiguration.Default,
fs,
builder => RazorExtensions.Register(builder)));
var service = new DefaultProjectEngineFactoryService(
projectManager.Object,
projectEngineFactory.Object,
new Lazy<IProjectEngineFactory, ICustomProjectEngineFactoryMetadata>[0]);
return service;
}
}
}

View File

@ -2,7 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.AspNetCore.Mvc.Razor.Extensions;
using System.IO;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.CodeAnalysis.Razor;
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
@ -13,23 +13,48 @@ namespace Microsoft.VisualStudio.Editor.Razor
{
public class DefaultImportDocumentManagerTest : ForegroundDispatcherTestBase
{
public DefaultImportDocumentManagerTest()
{
ProjectPath = "C:\\path\\to\\project\\project.csproj";
FileSystem = RazorProjectFileSystem.Create(Path.GetDirectoryName(ProjectPath));
ProjectEngine = RazorProjectEngine.Create(FallbackRazorConfiguration.MVC_2_1, FileSystem, b =>
{
// These tests rely on MVC's import behavior.
Microsoft.AspNetCore.Mvc.Razor.Extensions.RazorExtensions.Register(b);
});
}
private string FilePath { get; }
private string ProjectPath { get; }
private RazorProjectFileSystem FileSystem { get; }
private RazorProjectEngine ProjectEngine { get; }
[ForegroundFact]
public void OnSubscribed_StartsFileChangeTrackers()
{
// Arrange
var filePath = "C:\\path\\to\\project\\Views\\Home\\file.cshtml";
var projectPath = "C:\\path\\to\\project\\project.csproj";
var tracker = Mock.Of<VisualStudioDocumentTracker>(t => t.FilePath == filePath && t.ProjectPath == projectPath);
var projectEngineService = GetProjectEngineFactoryService();
var fileChangeTracker1 = new Mock<FileChangeTracker>();
fileChangeTracker1.Setup(f => f.StartListening()).Verifiable();
var tracker = Mock.Of<VisualStudioDocumentTracker>(
t => t.FilePath == "C:\\path\\to\\project\\Views\\Home\\file.cshtml" &&
t.ProjectPath == ProjectPath &&
t.ProjectSnapshot == Mock.Of<ProjectSnapshot>(p => p.GetProjectEngine() == ProjectEngine));
var fileChangeTrackerFactory = new Mock<FileChangeTrackerFactory>();
var fileChangeTracker1 = new Mock<FileChangeTracker>();
fileChangeTracker1
.Setup(f => f.StartListening())
.Verifiable();
fileChangeTrackerFactory
.Setup(f => f.Create("C:\\path\\to\\project\\Views\\Home\\_ViewImports.cshtml"))
.Returns(fileChangeTracker1.Object)
.Verifiable();
var fileChangeTracker2 = new Mock<FileChangeTracker>();
fileChangeTracker2.Setup(f => f.StartListening()).Verifiable();
fileChangeTracker2
.Setup(f => f.StartListening())
.Verifiable();
fileChangeTrackerFactory
.Setup(f => f.Create("C:\\path\\to\\project\\Views\\_ViewImports.cshtml"))
.Returns(fileChangeTracker2.Object)
@ -41,7 +66,7 @@ namespace Microsoft.VisualStudio.Editor.Razor
.Returns(fileChangeTracker3.Object)
.Verifiable();
var manager = new DefaultImportDocumentManager(Dispatcher, new DefaultErrorReporter(), fileChangeTrackerFactory.Object, projectEngineService);
var manager = new DefaultImportDocumentManager(Dispatcher, new DefaultErrorReporter(), fileChangeTrackerFactory.Object);
// Act
manager.OnSubscribed(tracker);
@ -57,10 +82,15 @@ namespace Microsoft.VisualStudio.Editor.Razor
public void OnSubscribed_AlreadySubscribed_DoesNothing()
{
// Arrange
var filePath = "C:\\path\\to\\project\\file.cshtml";
var projectPath = "C:\\path\\to\\project\\project.csproj";
var tracker = Mock.Of<VisualStudioDocumentTracker>(t => t.FilePath == filePath && t.ProjectPath == projectPath);
var projectEngineService = GetProjectEngineFactoryService();
var tracker = Mock.Of<VisualStudioDocumentTracker>(
t => t.FilePath == "C:\\path\\to\\project\\file.cshtml" &&
t.ProjectPath == ProjectPath &&
t.ProjectSnapshot == Mock.Of<ProjectSnapshot>(p => p.GetProjectEngine() == ProjectEngine));
var anotherTracker = Mock.Of<VisualStudioDocumentTracker>(
t => t.FilePath == "C:\\path\\to\\project\\anotherFile.cshtml" &&
t.ProjectPath == ProjectPath &&
t.ProjectSnapshot == Mock.Of<ProjectSnapshot>(p => p.GetProjectEngine() == ProjectEngine));
var callCount = 0;
var fileChangeTrackerFactory = new Mock<FileChangeTrackerFactory>();
@ -69,12 +99,9 @@ namespace Microsoft.VisualStudio.Editor.Razor
.Returns(Mock.Of<FileChangeTracker>())
.Callback(() => callCount++);
var manager = new DefaultImportDocumentManager(Dispatcher, new DefaultErrorReporter(), fileChangeTrackerFactory.Object, projectEngineService);
var manager = new DefaultImportDocumentManager(Dispatcher, new DefaultErrorReporter(), fileChangeTrackerFactory.Object);
manager.OnSubscribed(tracker); // Start tracking the import.
var anotherFilePath = "C:\\path\\to\\project\\anotherFile.cshtml";
var anotherTracker = Mock.Of<VisualStudioDocumentTracker>(t => t.FilePath == anotherFilePath && t.ProjectPath == projectPath);
// Act
manager.OnSubscribed(anotherTracker);
@ -86,20 +113,22 @@ namespace Microsoft.VisualStudio.Editor.Razor
public void OnUnsubscribed_StopsFileChangeTracker()
{
// Arrange
var filePath = "C:\\path\\to\\project\\file.cshtml";
var projectPath = "C:\\path\\to\\project\\project.csproj";
var tracker = Mock.Of<VisualStudioDocumentTracker>(t => t.FilePath == filePath && t.ProjectPath == projectPath);
var projectEngineService = GetProjectEngineFactoryService();
var tracker = Mock.Of<VisualStudioDocumentTracker>(
t => t.FilePath == "C:\\path\\to\\project\\file.cshtml" &&
t.ProjectPath == ProjectPath &&
t.ProjectSnapshot == Mock.Of<ProjectSnapshot>(p => p.GetProjectEngine() == ProjectEngine));
var fileChangeTracker = new Mock<FileChangeTracker>();
fileChangeTracker.Setup(f => f.StopListening()).Verifiable();
var fileChangeTrackerFactory = new Mock<FileChangeTrackerFactory>(MockBehavior.Strict);
var fileChangeTracker = new Mock<FileChangeTracker>();
fileChangeTracker
.Setup(f => f.StopListening())
.Verifiable();
fileChangeTrackerFactory
.Setup(f => f.Create("C:\\path\\to\\project\\_ViewImports.cshtml"))
.Returns(fileChangeTracker.Object)
.Verifiable();
var manager = new DefaultImportDocumentManager(Dispatcher, new DefaultErrorReporter(), fileChangeTrackerFactory.Object, projectEngineService);
var manager = new DefaultImportDocumentManager(Dispatcher, new DefaultErrorReporter(), fileChangeTrackerFactory.Object);
manager.OnSubscribed(tracker); // Start tracking the import.
// Act
@ -114,49 +143,33 @@ namespace Microsoft.VisualStudio.Editor.Razor
public void OnUnsubscribed_AnotherDocumentTrackingImport_DoesNotStopFileChangeTracker()
{
// Arrange
var filePath = "C:\\path\\to\\project\\file.cshtml";
var projectPath = "C:\\path\\to\\project\\project.csproj";
var tracker = Mock.Of<VisualStudioDocumentTracker>(t => t.FilePath == filePath && t.ProjectPath == projectPath);
var projectEngineService = GetProjectEngineFactoryService();
var tracker = Mock.Of<VisualStudioDocumentTracker>(
t => t.FilePath == "C:\\path\\to\\project\\file.cshtml" &&
t.ProjectPath == ProjectPath &&
t.ProjectSnapshot == Mock.Of<ProjectSnapshot>(p => p.GetProjectEngine() == ProjectEngine));
var anotherTracker = Mock.Of<VisualStudioDocumentTracker>(
t => t.FilePath == "C:\\path\\to\\project\\anotherFile.cshtml" &&
t.ProjectPath == ProjectPath &&
t.ProjectSnapshot == Mock.Of<ProjectSnapshot>(p => p.GetProjectEngine() == ProjectEngine));
var fileChangeTrackerFactory = new Mock<FileChangeTrackerFactory>();
var fileChangeTracker = new Mock<FileChangeTracker>();
fileChangeTracker
.Setup(f => f.StopListening())
.Throws(new InvalidOperationException());
var fileChangeTrackerFactory = new Mock<FileChangeTrackerFactory>();
fileChangeTrackerFactory
.Setup(f => f.Create(It.IsAny<string>()))
.Returns(fileChangeTracker.Object);
var manager = new DefaultImportDocumentManager(Dispatcher, new DefaultErrorReporter(), fileChangeTrackerFactory.Object, projectEngineService);
var manager = new DefaultImportDocumentManager(Dispatcher, new DefaultErrorReporter(), fileChangeTrackerFactory.Object);
manager.OnSubscribed(tracker); // Starts tracking import for the first document.
var anotherFilePath = "C:\\path\\to\\project\\anotherFile.cshtml";
var anotherTracker = Mock.Of<VisualStudioDocumentTracker>(t => t.FilePath == anotherFilePath && t.ProjectPath == projectPath);
manager.OnSubscribed(anotherTracker); // Starts tracking import for the second document.
// Act & Assert (Does not throw)
manager.OnUnsubscribed(tracker);
}
private RazorProjectEngineFactoryService GetProjectEngineFactoryService()
{
var projectManager = new Mock<ProjectSnapshotManager>();
projectManager.Setup(p => p.Projects).Returns(Array.Empty<ProjectSnapshot>());
var projectEngineFactory = new Mock<IFallbackProjectEngineFactory>();
projectEngineFactory.Setup(s => s.Create(It.IsAny<RazorConfiguration>(), It.IsAny<RazorProjectFileSystem>(), It.IsAny<Action<RazorProjectEngineBuilder>>()))
.Returns<RazorConfiguration, RazorProjectFileSystem, Action<RazorProjectEngineBuilder>>(
(c, fs, b) => RazorProjectEngine.Create(
RazorConfiguration.Default,
fs,
builder => RazorExtensions.Register(builder)));
var service = new DefaultProjectEngineFactoryService(
projectManager.Object,
projectEngineFactory.Object,
new Lazy<IProjectEngineFactory, ICustomProjectEngineFactoryMetadata>[0]);
return service;
manager.OnUnsubscribed(tracker);
}
}
}

View File

@ -5,19 +5,19 @@ using System;
using System.Linq;
using System.Reflection;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Razor;
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
using Moq;
using Microsoft.VisualStudio.Editor.Razor;
using Xunit;
using Mvc1_X = Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X;
using MvcLatest = Microsoft.AspNetCore.Mvc.Razor.Extensions;
namespace Microsoft.VisualStudio.Editor.Razor
namespace Microsoft.CodeAnalysis.Razor
{
public class DefaultProjectEngineFactoryServiceTest
// Testing this here because we need references to the MVC factories.
public class DefaultProjectSnapshotProjectEngineFactoryTest
{
public DefaultProjectEngineFactoryServiceTest()
public DefaultProjectSnapshotProjectEngineFactoryTest()
{
Project project = null;
@ -34,12 +34,17 @@ namespace Microsoft.VisualStudio.Editor.Razor
HostProject_For_2_0 = new HostProject("/TestPath/SomePath/Test.csproj", FallbackRazorConfiguration.MVC_2_0);
HostProject_For_2_1 = new HostProject(
"/TestPath/SomePath/Test.csproj",
"/TestPath/SomePath/Test.csproj",
new ProjectSystemRazorConfiguration(RazorLanguageVersion.Version_2_1, "MVC-2.1", Array.Empty<RazorExtension>()));
HostProject_For_UnknownConfiguration = new HostProject(
"/TestPath/SomePath/Test.csproj",
new ProjectSystemRazorConfiguration(RazorLanguageVersion.Version_2_1, "Blazor-0.1", Array.Empty<RazorExtension>()));
Snapshot_For_1_0 = new DefaultProjectSnapshot(new ProjectState(Workspace.Services, HostProject_For_1_0, WorkspaceProject));
Snapshot_For_1_1 = new DefaultProjectSnapshot(new ProjectState(Workspace.Services, HostProject_For_1_1, WorkspaceProject));
Snapshot_For_2_0 = new DefaultProjectSnapshot(new ProjectState(Workspace.Services, HostProject_For_2_0, WorkspaceProject));
Snapshot_For_2_1 = new DefaultProjectSnapshot(new ProjectState(Workspace.Services, HostProject_For_2_1, WorkspaceProject));
Snapshot_For_UnknownConfiguration = new DefaultProjectSnapshot(new ProjectState(Workspace.Services, HostProject_For_UnknownConfiguration, WorkspaceProject));
CustomFactories = new Lazy<IProjectEngineFactory, ICustomProjectEngineFactoryMetadata>[]
{
@ -74,7 +79,16 @@ namespace Microsoft.VisualStudio.Editor.Razor
private HostProject HostProject_For_UnknownConfiguration { get; }
// We don't actually look at the project, we rely on the ProjectStateManager
private ProjectSnapshot Snapshot_For_1_0 { get; }
private ProjectSnapshot Snapshot_For_1_1 { get; }
private ProjectSnapshot Snapshot_For_2_0 { get; }
private ProjectSnapshot Snapshot_For_2_1 { get; }
private ProjectSnapshot Snapshot_For_UnknownConfiguration { get; }
private Project WorkspaceProject { get; }
private Workspace Workspace { get; }
@ -83,14 +97,12 @@ namespace Microsoft.VisualStudio.Editor.Razor
public void Create_CreatesDesignTimeTemplateEngine_ForVersion2_1()
{
// Arrange
var projectManager = new TestProjectSnapshotManager(Workspace);
projectManager.HostProjectAdded(HostProject_For_2_1);
projectManager.WorkspaceProjectAdded(WorkspaceProject);
var snapshot = Snapshot_For_2_1;
var factoryService = new DefaultProjectEngineFactoryService(projectManager, FallbackFactory, CustomFactories);
var factory = new DefaultProjectSnapshotProjectEngineFactory(FallbackFactory, CustomFactories);
// Act
var engine = factoryService.Create("/TestPath/SomePath/", b =>
var engine = factory.Create(snapshot, b =>
{
b.Features.Add(new MyCoolNewFeature());
});
@ -106,14 +118,12 @@ namespace Microsoft.VisualStudio.Editor.Razor
public void Create_CreatesDesignTimeTemplateEngine_ForVersion2_0()
{
// Arrange
var projectManager = new TestProjectSnapshotManager(Workspace);
projectManager.HostProjectAdded(HostProject_For_2_0);
projectManager.WorkspaceProjectAdded(WorkspaceProject);
var snapshot = Snapshot_For_2_0;
var factoryService = new DefaultProjectEngineFactoryService(projectManager, FallbackFactory, CustomFactories);
var factory = new DefaultProjectSnapshotProjectEngineFactory(FallbackFactory, CustomFactories);
// Act
var engine = factoryService.Create("/TestPath/SomePath/", b =>
var engine = factory.Create(snapshot, b =>
{
b.Features.Add(new MyCoolNewFeature());
});
@ -129,14 +139,12 @@ namespace Microsoft.VisualStudio.Editor.Razor
public void Create_CreatesTemplateEngine_ForVersion1_1()
{
// Arrange
var projectManager = new TestProjectSnapshotManager(Workspace);
projectManager.HostProjectAdded(HostProject_For_1_1);
projectManager.WorkspaceProjectAdded(WorkspaceProject);
var snapshot = Snapshot_For_1_1;
var factoryService = new DefaultProjectEngineFactoryService(projectManager, FallbackFactory, CustomFactories);
var factory = new DefaultProjectSnapshotProjectEngineFactory(FallbackFactory, CustomFactories);
// Act
var engine = factoryService.Create("/TestPath/SomePath/", b =>
var engine = factory.Create(snapshot, b =>
{
b.Features.Add(new MyCoolNewFeature());
});
@ -152,14 +160,12 @@ namespace Microsoft.VisualStudio.Editor.Razor
public void Create_DoesNotSupportViewComponentTagHelpers_ForVersion1_0()
{
// Arrange
var projectManager = new TestProjectSnapshotManager(Workspace);
projectManager.HostProjectAdded(HostProject_For_1_0);
projectManager.WorkspaceProjectAdded(WorkspaceProject);
var snapshot = Snapshot_For_1_0;
var factoryService = new DefaultProjectEngineFactoryService(projectManager, FallbackFactory, CustomFactories);
var factory = new DefaultProjectSnapshotProjectEngineFactory(FallbackFactory, CustomFactories);
// Act
var engine = factoryService.Create("/TestPath/SomePath/", b =>
var engine = factory.Create(snapshot, b =>
{
b.Features.Add(new MyCoolNewFeature());
});
@ -171,40 +177,15 @@ namespace Microsoft.VisualStudio.Editor.Razor
Assert.Empty(engine.Engine.Features.OfType<MvcLatest.ViewComponentTagHelperPass>());
}
[Fact]
public void Create_UnknownProject_UsesVersion2_0()
{
// Arrange
var projectManager = new TestProjectSnapshotManager(Workspace);
var factoryService = new DefaultProjectEngineFactoryService(projectManager, FallbackFactory, CustomFactories);
// Act
var engine = factoryService.Create("/TestPath/DifferentPath/", b =>
{
b.Features.Add(new MyCoolNewFeature());
});
// Assert
Assert.Single(engine.Engine.Features.OfType<MyCoolNewFeature>());
Assert.Single(engine.Engine.Features.OfType<DefaultTagHelperDescriptorProvider>());
Assert.Single(engine.Engine.Features.OfType<MvcLatest.ViewComponentTagHelperDescriptorProvider>());
Assert.Single(engine.Engine.Features.OfType<MvcLatest.MvcViewDocumentClassifierPass>());
Assert.Single(engine.Engine.Features.OfType<MvcLatest.ViewComponentTagHelperPass>());
}
[Fact]
public void Create_ForUnknownConfiguration_UsesFallbackFactory()
{
// Arrange
var projectManager = new TestProjectSnapshotManager(Workspace);
projectManager.HostProjectAdded(HostProject_For_UnknownConfiguration);
projectManager.WorkspaceProjectAdded(WorkspaceProject);
var snapshot = Snapshot_For_UnknownConfiguration;
var factoryService = new DefaultProjectEngineFactoryService(projectManager, FallbackFactory, CustomFactories);
var factory = new DefaultProjectSnapshotProjectEngineFactory(FallbackFactory, CustomFactories);
// Act
var engine = factoryService.Create("/TestPath/SomePath/", b =>
var engine = factory.Create(snapshot, b =>
{
b.Features.Add(new MyCoolNewFeature());
});
@ -221,18 +202,5 @@ namespace Microsoft.VisualStudio.Editor.Razor
{
public RazorEngine Engine { get; set; }
}
private class TestProjectSnapshotManager : DefaultProjectSnapshotManager
{
public TestProjectSnapshotManager(Workspace workspace)
: base(
Mock.Of<ForegroundDispatcher>(),
Mock.Of<ErrorReporter>(),
Mock.Of<ProjectSnapshotWorker>(),
Enumerable.Empty<ProjectSnapshotChangeTrigger>(),
workspace)
{
}
}
}
}

View File

@ -3,8 +3,10 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Razor;
using Microsoft.CodeAnalysis.Razor.Editor;
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
@ -18,29 +20,90 @@ namespace Microsoft.VisualStudio.Editor.Razor
{
public class DefaultVisualStudioDocumentTrackerTest : ForegroundDispatcherTestBase
{
private IContentType RazorCoreContentType { get; } = Mock.Of<IContentType>(c => c.IsOfType(RazorLanguage.CoreContentType) == true);
public DefaultVisualStudioDocumentTrackerTest()
{
RazorCoreContentType = Mock.Of<IContentType>(c => c.IsOfType(RazorLanguage.ContentType) == true);
TextBuffer = Mock.Of<ITextBuffer>(b => b.ContentType == RazorCoreContentType);
private ITextBuffer TextBuffer => Mock.Of<ITextBuffer>(b => b.ContentType == RazorCoreContentType);
FilePath = "C:/Some/Path/TestDocumentTracker.cshtml";
ProjectPath = "C:/Some/Path/TestProject.csproj";
private string FilePath => "C:/Some/Path/TestDocumentTracker.cshtml";
ImportDocumentManager = Mock.Of<ImportDocumentManager>();
WorkspaceEditorSettings = new DefaultWorkspaceEditorSettings(Mock.Of<ForegroundDispatcher>(), Mock.Of<EditorSettingsManager>());
private string ProjectPath => "C:/Some/Path/TestProject.csproj";
TagHelperResolver = new TestTagHelperResolver();
SomeTagHelpers = new List<TagHelperDescriptor>()
{
TagHelperDescriptorBuilder.Create("test", "test").Build(),
};
private ProjectSnapshotManager ProjectManager => Mock.Of<ProjectSnapshotManager>(p => p.Projects == new List<ProjectSnapshot>());
HostServices = TestServices.Create(
new IWorkspaceService[] { },
new ILanguageService[] { TagHelperResolver, });
private WorkspaceEditorSettings WorkspaceEditorSettings => new DefaultWorkspaceEditorSettings(Dispatcher, Mock.Of<EditorSettingsManager>());
Workspace = TestWorkspace.Create(HostServices, w =>
{
WorkspaceProject = w.AddProject(ProjectInfo.Create(
ProjectId.CreateNewId(),
new VersionStamp(),
"Test1",
"TestAssembly",
LanguageNames.CSharp,
filePath: ProjectPath));
});
private Workspace Workspace => TestWorkspace.Create();
ProjectManager = new TestProjectSnapshotManager(Dispatcher, Workspace) { AllowNotifyListeners = true };
private ImportDocumentManager ImportDocumentManager => Mock.Of<ImportDocumentManager>();
HostProject = new HostProject(ProjectPath, FallbackRazorConfiguration.MVC_2_1);
OtherHostProject = new HostProject(ProjectPath, FallbackRazorConfiguration.MVC_2_0);
[Fact]
DocumentTracker = new DefaultVisualStudioDocumentTracker(
Dispatcher,
FilePath,
ProjectPath,
ProjectManager,
WorkspaceEditorSettings,
Workspace,
TextBuffer,
ImportDocumentManager);
}
private IContentType RazorCoreContentType { get; }
private ITextBuffer TextBuffer { get; }
private string FilePath { get; }
private string ProjectPath { get; }
private HostProject HostProject { get; }
private HostProject OtherHostProject { get; }
private Project WorkspaceProject { get; set; }
private ImportDocumentManager ImportDocumentManager { get; }
private WorkspaceEditorSettings WorkspaceEditorSettings { get; }
private List<TagHelperDescriptor> SomeTagHelpers { get; }
private TestTagHelperResolver TagHelperResolver { get; }
private ProjectSnapshotManagerBase ProjectManager { get; }
private HostServices HostServices { get; }
private Workspace Workspace { get; }
private DefaultVisualStudioDocumentTracker DocumentTracker { get; }
[ForegroundFact]
public void EditorSettingsManager_Changed_TriggersContextChanged()
{
// Arrange
var documentTracker = new DefaultVisualStudioDocumentTracker(Dispatcher, FilePath, ProjectPath, ProjectManager, WorkspaceEditorSettings, Workspace, TextBuffer, ImportDocumentManager);
var called = false;
documentTracker.ContextChanged += (sender, args) =>
DocumentTracker.ContextChanged += (sender, args) =>
{
Assert.Equal(ContextChangeKind.EditorSettingsChanged, args.Kind);
called = true;
@ -48,103 +111,109 @@ namespace Microsoft.VisualStudio.Editor.Razor
};
// Act
documentTracker.EditorSettingsManager_Changed(null, null);
DocumentTracker.EditorSettingsManager_Changed(null, null);
// Assert
Assert.True(called);
}
[Fact]
public void ProjectManager_Changed_ProjectChanged_TriggersContextChanged()
[ForegroundFact]
public void ProjectManager_Changed_ProjectAdded_TriggersContextChanged()
{
// Arrange
Project project = null;
var workspace = TestWorkspace.Create(ws =>
{
project = ws.AddProject(ProjectInfo.Create(ProjectId.CreateNewId(), new VersionStamp(), "Test1", "TestAssembly", LanguageNames.CSharp, filePath: "C:/Some/Path/TestProject.csproj"));
});
var documentTracker = new DefaultVisualStudioDocumentTracker(Dispatcher, FilePath, ProjectPath, ProjectManager, WorkspaceEditorSettings, workspace, TextBuffer, ImportDocumentManager);
ProjectManager.HostProjectAdded(HostProject);
ProjectManager.WorkspaceProjectAdded(WorkspaceProject);
var projectSnapshot = new DefaultProjectSnapshot(new HostProject(project.FilePath, RazorConfiguration.Default), project);
var projectChangedArgs = new ProjectChangeEventArgs(projectSnapshot, ProjectChangeKind.Changed);
var e = new ProjectChangeEventArgs(ProjectPath, ProjectChangeKind.ProjectAdded);
var called = false;
documentTracker.ContextChanged += (sender, args) =>
DocumentTracker.ContextChanged += (sender, args) =>
{
Assert.Equal(ContextChangeKind.ProjectChanged, args.Kind);
called = true;
Assert.Same(ProjectManager.GetLoadedProject(DocumentTracker.ProjectPath), DocumentTracker.ProjectSnapshot);
};
// Act
documentTracker.ProjectManager_Changed(null, projectChangedArgs);
DocumentTracker.ProjectManager_Changed(ProjectManager, e);
// Assert
Assert.True(called);
}
[Fact]
public void ProjectManager_Changed_TagHelpersChanged_TriggersContextChanged()
[ForegroundFact]
public void ProjectManager_Changed_ProjectChanged_TriggersContextChanged()
{
// Arrange
Project project = null;
var workspace = TestWorkspace.Create(ws =>
{
project = ws.AddProject(ProjectInfo.Create(ProjectId.CreateNewId(), new VersionStamp(), "Test1", "TestAssembly", LanguageNames.CSharp, filePath: "C:/Some/Path/TestProject.csproj"));
});
var documentTracker = new DefaultVisualStudioDocumentTracker(Dispatcher, FilePath, ProjectPath, ProjectManager, WorkspaceEditorSettings, workspace, TextBuffer, ImportDocumentManager);
ProjectManager.HostProjectAdded(HostProject);
ProjectManager.WorkspaceProjectAdded(WorkspaceProject);
var projectSnapshot = new DefaultProjectSnapshot(new HostProject(project.FilePath, RazorConfiguration.Default), project);
var projectChangedArgs = new ProjectChangeEventArgs(projectSnapshot, ProjectChangeKind.TagHelpersChanged);
var e = new ProjectChangeEventArgs(ProjectPath, ProjectChangeKind.ProjectChanged);
var called = false;
documentTracker.ContextChanged += (sender, args) =>
DocumentTracker.ContextChanged += (sender, args) =>
{
Assert.Equal(ContextChangeKind.TagHelpersChanged, args.Kind);
Assert.Equal(ContextChangeKind.ProjectChanged, args.Kind);
called = true;
Assert.Same(ProjectManager.GetLoadedProject(DocumentTracker.ProjectPath), DocumentTracker.ProjectSnapshot);
};
// Act
documentTracker.ProjectManager_Changed(null, projectChangedArgs);
DocumentTracker.ProjectManager_Changed(ProjectManager, e);
// Assert
Assert.True(called);
}
[Fact]
[ForegroundFact]
public void ProjectManager_Changed_ProjectRemoved_TriggersContextChanged_WithEphemeralProject()
{
// Arrange
var e = new ProjectChangeEventArgs(ProjectPath, ProjectChangeKind.ProjectRemoved);
var called = false;
DocumentTracker.ContextChanged += (sender, args) =>
{
// This can be called both with tag helper and project changes.
called = true;
Assert.IsType<EphemeralProjectSnapshot>(DocumentTracker.ProjectSnapshot);
};
// Act
DocumentTracker.ProjectManager_Changed(ProjectManager, e);
// Assert
Assert.True(called);
}
[ForegroundFact]
public void ProjectManager_Changed_IgnoresUnknownProject()
{
// Arrange
Project project = null;
var workspace = TestWorkspace.Create(ws =>
{
project = ws.AddProject(ProjectInfo.Create(ProjectId.CreateNewId(), new VersionStamp(), "Test1", "TestAssembly", LanguageNames.CSharp, filePath: "C:/Some/Other/Path/TestProject.csproj"));
});
var documentTracker = new DefaultVisualStudioDocumentTracker(Dispatcher, FilePath, ProjectPath, ProjectManager, WorkspaceEditorSettings, workspace, TextBuffer, ImportDocumentManager);
var projectSnapshot = new DefaultProjectSnapshot(new HostProject(project.FilePath, RazorConfiguration.Default), project);
var projectChangedArgs = new ProjectChangeEventArgs(projectSnapshot, ProjectChangeKind.Changed);
var e = new ProjectChangeEventArgs("c:/OtherPath/OtherProject.csproj", ProjectChangeKind.ProjectChanged);
var called = false;
documentTracker.ContextChanged += (sender, args) =>
DocumentTracker.ContextChanged += (sender, args) =>
{
called = true;
};
// Act
documentTracker.ProjectManager_Changed(null, projectChangedArgs);
DocumentTracker.ProjectManager_Changed(ProjectManager, e);
// Assert
Assert.False(called);
}
[Fact]
[ForegroundFact]
public void Import_Changed_ImportAssociatedWithDocument_TriggersContextChanged()
{
// Arrange
var documentTracker = new DefaultVisualStudioDocumentTracker(Dispatcher, FilePath, ProjectPath, ProjectManager, WorkspaceEditorSettings, Workspace, TextBuffer, ImportDocumentManager);
var called = false;
documentTracker.ContextChanged += (sender, args) =>
DocumentTracker.ContextChanged += (sender, args) =>
{
Assert.Equal(ContextChangeKind.ImportsChanged, args.Kind);
called = true;
@ -153,19 +222,17 @@ namespace Microsoft.VisualStudio.Editor.Razor
var importChangedArgs = new ImportChangedEventArgs("path/to/import", FileChangeKind.Changed, new[] { FilePath });
// Act
documentTracker.Import_Changed(null, importChangedArgs);
DocumentTracker.Import_Changed(null, importChangedArgs);
// Assert
Assert.True(called);
}
[Fact]
[ForegroundFact]
public void Import_Changed_UnrelatedImport_DoesNothing()
{
// Arrange
var documentTracker = new DefaultVisualStudioDocumentTracker(Dispatcher, FilePath, ProjectPath, ProjectManager, WorkspaceEditorSettings, Workspace, TextBuffer, ImportDocumentManager);
documentTracker.ContextChanged += (sender, args) =>
DocumentTracker.ContextChanged += (sender, args) =>
{
throw new InvalidOperationException();
};
@ -173,152 +240,254 @@ namespace Microsoft.VisualStudio.Editor.Razor
var importChangedArgs = new ImportChangedEventArgs("path/to/import", FileChangeKind.Changed, new[] { "path/to/differentfile" });
// Act & Assert (Does not throw)
documentTracker.Import_Changed(null, importChangedArgs);
DocumentTracker.Import_Changed(null, importChangedArgs);
}
[Fact]
[ForegroundFact]
public void Subscribe_SetsSupportedProjectAndTriggersContextChanged()
{
// Arrange
var documentTracker = new DefaultVisualStudioDocumentTracker(Dispatcher, FilePath, ProjectPath, ProjectManager, WorkspaceEditorSettings, Workspace, TextBuffer, ImportDocumentManager);
var called = false;
documentTracker.ContextChanged += (sender, args) =>
DocumentTracker.ContextChanged += (sender, args) =>
{
called = true;
Assert.Equal(ContextChangeKind.ProjectChanged, args.Kind);
called = true; // This will trigger both ContextChanged and TagHelprsChanged
};
// Act
documentTracker.Subscribe();
DocumentTracker.Subscribe();
// Assert
Assert.True(called);
Assert.True(documentTracker.IsSupportedProject);
Assert.True(DocumentTracker.IsSupportedProject);
}
[Fact]
[ForegroundFact]
public void Unsubscribe_ResetsSupportedProjectAndTriggersContextChanged()
{
// Arrange
var documentTracker = new DefaultVisualStudioDocumentTracker(Dispatcher, FilePath, ProjectPath, ProjectManager, WorkspaceEditorSettings, Workspace, TextBuffer, ImportDocumentManager);
// Subscribe once to set supported project
documentTracker.Subscribe();
DocumentTracker.Subscribe();
var called = false;
documentTracker.ContextChanged += (sender, args) =>
DocumentTracker.ContextChanged += (sender, args) =>
{
called = true;
Assert.Equal(ContextChangeKind.ProjectChanged, args.Kind);
};
// Act
documentTracker.Unsubscribe();
DocumentTracker.Unsubscribe();
// Assert
Assert.False(documentTracker.IsSupportedProject);
Assert.False(DocumentTracker.IsSupportedProject);
Assert.True(called);
}
[Fact]
[ForegroundFact]
public void AddTextView_AddsToTextViewCollection()
{
// Arrange
var documentTracker = new DefaultVisualStudioDocumentTracker(Dispatcher, FilePath, ProjectPath, ProjectManager, WorkspaceEditorSettings, Workspace, TextBuffer, ImportDocumentManager);
var textView = Mock.Of<ITextView>();
// Act
documentTracker.AddTextView(textView);
DocumentTracker.AddTextView(textView);
// Assert
Assert.Collection(documentTracker.TextViews, v => Assert.Same(v, textView));
Assert.Collection(DocumentTracker.TextViews, v => Assert.Same(v, textView));
}
[Fact]
[ForegroundFact]
public void AddTextView_DoesNotAddDuplicateTextViews()
{
// Arrange
var documentTracker = new DefaultVisualStudioDocumentTracker(Dispatcher, FilePath, ProjectPath, ProjectManager, WorkspaceEditorSettings, Workspace, TextBuffer, ImportDocumentManager);
var textView = Mock.Of<ITextView>();
// Act
documentTracker.AddTextView(textView);
documentTracker.AddTextView(textView);
DocumentTracker.AddTextView(textView);
DocumentTracker.AddTextView(textView);
// Assert
Assert.Collection(documentTracker.TextViews, v => Assert.Same(v, textView));
Assert.Collection(DocumentTracker.TextViews, v => Assert.Same(v, textView));
}
[Fact]
[ForegroundFact]
public void AddTextView_AddsMultipleTextViewsToCollection()
{
// Arrange
var documentTracker = new DefaultVisualStudioDocumentTracker(Dispatcher, FilePath, ProjectPath, ProjectManager, WorkspaceEditorSettings, Workspace, TextBuffer, ImportDocumentManager);
var textView1 = Mock.Of<ITextView>();
var textView2 = Mock.Of<ITextView>();
// Act
documentTracker.AddTextView(textView1);
documentTracker.AddTextView(textView2);
DocumentTracker.AddTextView(textView1);
DocumentTracker.AddTextView(textView2);
// Assert
Assert.Collection(
documentTracker.TextViews,
DocumentTracker.TextViews,
v => Assert.Same(v, textView1),
v => Assert.Same(v, textView2));
}
[Fact]
[ForegroundFact]
public void RemoveTextView_RemovesTextViewFromCollection_SingleItem()
{
// Arrange
var documentTracker = new DefaultVisualStudioDocumentTracker(Dispatcher, FilePath, ProjectPath, ProjectManager, WorkspaceEditorSettings, Workspace, TextBuffer, ImportDocumentManager);
var textView = Mock.Of<ITextView>();
documentTracker.AddTextView(textView);
DocumentTracker.AddTextView(textView);
// Act
documentTracker.RemoveTextView(textView);
DocumentTracker.RemoveTextView(textView);
// Assert
Assert.Empty(documentTracker.TextViews);
Assert.Empty(DocumentTracker.TextViews);
}
[Fact]
[ForegroundFact]
public void RemoveTextView_RemovesTextViewFromCollection_MultipleItems()
{
// Arrange
var documentTracker = new DefaultVisualStudioDocumentTracker(Dispatcher, FilePath, ProjectPath, ProjectManager, WorkspaceEditorSettings, Workspace, TextBuffer, ImportDocumentManager);
var textView1 = Mock.Of<ITextView>();
var textView2 = Mock.Of<ITextView>();
var textView3 = Mock.Of<ITextView>();
documentTracker.AddTextView(textView1);
documentTracker.AddTextView(textView2);
documentTracker.AddTextView(textView3);
DocumentTracker.AddTextView(textView1);
DocumentTracker.AddTextView(textView2);
DocumentTracker.AddTextView(textView3);
// Act
documentTracker.RemoveTextView(textView2);
DocumentTracker.RemoveTextView(textView2);
// Assert
Assert.Collection(
documentTracker.TextViews,
DocumentTracker.TextViews,
v => Assert.Same(v, textView1),
v => Assert.Same(v, textView3));
}
[Fact]
[ForegroundFact]
public void RemoveTextView_NoopsWhenRemovingTextViewNotInCollection()
{
// Arrange
var documentTracker = new DefaultVisualStudioDocumentTracker(Dispatcher, FilePath, ProjectPath, ProjectManager, WorkspaceEditorSettings, Workspace, TextBuffer, ImportDocumentManager);
var textView1 = Mock.Of<ITextView>();
documentTracker.AddTextView(textView1);
DocumentTracker.AddTextView(textView1);
var textView2 = Mock.Of<ITextView>();
// Act
documentTracker.RemoveTextView(textView2);
DocumentTracker.RemoveTextView(textView2);
// Assert
Assert.Collection(documentTracker.TextViews, v => Assert.Same(v, textView1));
Assert.Collection(DocumentTracker.TextViews, v => Assert.Same(v, textView1));
}
[ForegroundFact]
public void Subscribed_InitializesEphemeralProjectSnapshot()
{
// Arrange
// Act
DocumentTracker.Subscribe();
// Assert
Assert.IsType<EphemeralProjectSnapshot>(DocumentTracker.ProjectSnapshot);
}
[ForegroundFact]
public void Subscribed_InitializesRealProjectSnapshot()
{
// Arrange
ProjectManager.HostProjectAdded(HostProject);
// Act
DocumentTracker.Subscribe();
// Assert
Assert.IsType<DefaultProjectSnapshot>(DocumentTracker.ProjectSnapshot);
}
[ForegroundFact]
public async Task Subscribed_ListensToProjectChanges()
{
// Arrange
ProjectManager.HostProjectAdded(HostProject);
DocumentTracker.Subscribe();
await DocumentTracker.PendingTagHelperTask;
// There can be multiple args here because the tag helpers will return
// immediately and trigger another ContextChanged.
List<ContextChangeEventArgs> args = new List<ContextChangeEventArgs>();
DocumentTracker.ContextChanged += (sender, e) => { args.Add(e); };
// Act
ProjectManager.HostProjectChanged(OtherHostProject);
await DocumentTracker.PendingTagHelperTask;
// Assert
var snapshot = Assert.IsType<DefaultProjectSnapshot>(DocumentTracker.ProjectSnapshot);
Assert.Same(OtherHostProject, snapshot.HostProject);
Assert.Collection(
args,
e => Assert.Equal(ContextChangeKind.ProjectChanged, e.Kind),
e => Assert.Equal(ContextChangeKind.TagHelpersChanged, e.Kind));
}
[ForegroundFact]
public async Task Subscribed_ListensToProjectRemoval()
{
// Arrange
ProjectManager.HostProjectAdded(HostProject);
DocumentTracker.Subscribe();
await DocumentTracker.PendingTagHelperTask;
List<ContextChangeEventArgs> args = new List<ContextChangeEventArgs>();
DocumentTracker.ContextChanged += (sender, e) => { args.Add(e); };
// Act
ProjectManager.HostProjectRemoved(HostProject);
await DocumentTracker.PendingTagHelperTask;
// Assert
Assert.IsType<EphemeralProjectSnapshot>(DocumentTracker.ProjectSnapshot);
Assert.Collection(
args,
e => Assert.Equal(ContextChangeKind.ProjectChanged, e.Kind),
e => Assert.Equal(ContextChangeKind.TagHelpersChanged, e.Kind));
}
[ForegroundFact]
public async Task Subscribed_ListensToProjectChanges_ComputesTagHelpers()
{
// Arrange
TagHelperResolver.CompletionSource = new TaskCompletionSource<TagHelperResolutionResult>();
ProjectManager.HostProjectAdded(HostProject);
DocumentTracker.Subscribe();
// We haven't let the tag helpers complete yet
Assert.False(DocumentTracker.PendingTagHelperTask.IsCompleted);
Assert.Empty(DocumentTracker.TagHelpers);
List<ContextChangeEventArgs> args = new List<ContextChangeEventArgs>();
DocumentTracker.ContextChanged += (sender, e) => { args.Add(e); };
// Act
TagHelperResolver.CompletionSource.SetResult(new TagHelperResolutionResult(SomeTagHelpers, Array.Empty<RazorDiagnostic>()));
await DocumentTracker.PendingTagHelperTask;
// Assert
Assert.Same(DocumentTracker.TagHelpers, SomeTagHelpers);
Assert.Collection(
args,
e => Assert.Equal(ContextChangeKind.TagHelpersChanged, e.Kind));
}
}
}

View File

@ -4,13 +4,14 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Razor.Extensions;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.Language.Legacy;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Razor;
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
using Microsoft.VisualStudio.Test;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Editor;
@ -24,6 +25,16 @@ namespace Microsoft.VisualStudio.Editor.Razor
private const string TestLinePragmaFileName = "C:\\This\\Path\\Is\\Just\\For\\Line\\Pragmas.cshtml";
private const string TestProjectPath = "C:\\This\\Path\\Is\\Just\\For\\Project.csproj";
public DefaultVisualStudioRazorParserIntegrationTest()
{
Workspace = TestWorkspace.Create();
ProjectSnapshot = new EphemeralProjectSnapshot(Workspace.Services, TestProjectPath);
}
private ProjectSnapshot ProjectSnapshot { get; }
private Workspace Workspace { get; }
[ForegroundFact]
public async Task BufferChangeStartsFullReparseIfChangeOverlapsMultipleSpans()
{
@ -505,7 +516,7 @@ namespace Microsoft.VisualStudio.Editor.Razor
{
var textBuffer = new TestTextBuffer(originalSnapshot);
var documentTracker = CreateDocumentTracker(textBuffer);
var templateEngineFactory = CreateTemplateEngineFactory();
var templateEngineFactory = CreateProjectEngineFactory();
var parser = new DefaultVisualStudioRazorParser(
Dispatcher,
documentTracker,
@ -524,7 +535,7 @@ namespace Microsoft.VisualStudio.Editor.Razor
return new TestParserManager(parser);
}
private static RazorProjectEngineFactoryService CreateTemplateEngineFactory(
private static ProjectSnapshotProjectEngineFactory CreateProjectEngineFactory(
string path = TestLinePragmaFileName,
IEnumerable<TagHelperDescriptor> tagHelpers = null)
{
@ -541,10 +552,10 @@ namespace Microsoft.VisualStudio.Editor.Razor
}
});
var projectEngineFactoryService = Mock.Of<RazorProjectEngineFactoryService>(
service => service.Create(It.IsAny<string>(), It.IsAny<Action<RazorProjectEngineBuilder>>()) == projectEngine);
return projectEngineFactoryService;
return new TestProjectSnapshotProjectEngineFactory()
{
Engine = projectEngine,
};
}
private async Task RunTypeKeywordTestAsync(string keyword)
@ -584,7 +595,7 @@ namespace Microsoft.VisualStudio.Editor.Razor
#endif
}
private static VisualStudioDocumentTracker CreateDocumentTracker(Text.ITextBuffer textBuffer)
private VisualStudioDocumentTracker CreateDocumentTracker(Text.ITextBuffer textBuffer)
{
var focusedTextView = Mock.Of<ITextView>(textView => textView.HasAggregateFocus == true);
var documentTracker = Mock.Of<VisualStudioDocumentTracker>(tracker =>
@ -592,6 +603,7 @@ namespace Microsoft.VisualStudio.Editor.Razor
tracker.TextViews == new[] { focusedTextView } &&
tracker.FilePath == TestLinePragmaFileName &&
tracker.ProjectPath == TestProjectPath &&
tracker.ProjectSnapshot == ProjectSnapshot &&
tracker.IsSupportedProject == true);
textBuffer.Properties.AddProperty(typeof(VisualStudioDocumentTracker), documentTracker);

View File

@ -4,7 +4,9 @@
using System;
using System.Threading;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Razor;
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
using Microsoft.VisualStudio.Test;
using Microsoft.VisualStudio.Text;
using Moq;
@ -14,12 +16,32 @@ namespace Microsoft.VisualStudio.Editor.Razor
{
public class DefaultVisualStudioRazorParserTest : ForegroundDispatcherTestBase
{
private static VisualStudioDocumentTracker CreateDocumentTracker(bool isSupportedProject = true)
public DefaultVisualStudioRazorParserTest()
{
Workspace = TestWorkspace.Create();
ProjectSnapshot = new EphemeralProjectSnapshot(Workspace.Services, "c:\\SomeProject.csproj");
var engine = RazorProjectEngine.Create(RazorConfiguration.Default, RazorProjectFileSystem.Empty);
ProjectEngineFactory = Mock.Of<ProjectSnapshotProjectEngineFactory>(
f => f.Create(
It.IsAny<ProjectSnapshot>(),
It.IsAny<RazorProjectFileSystem>(),
It.IsAny<Action<RazorProjectEngineBuilder>>()) == engine);
}
private ProjectSnapshot ProjectSnapshot { get; }
private ProjectSnapshotProjectEngineFactory ProjectEngineFactory { get; }
private Workspace Workspace { get; }
private VisualStudioDocumentTracker CreateDocumentTracker(bool isSupportedProject = true)
{
var documentTracker = Mock.Of<VisualStudioDocumentTracker>(tracker =>
tracker.TextBuffer == new TestTextBuffer(new StringTextSnapshot(string.Empty)) &&
tracker.ProjectPath == "SomeProject.csproj" &&
tracker.FilePath == "SomeFilePath.cshtml" &&
tracker.ProjectPath == "c:\\SomeProject.csproj" &&
tracker.ProjectSnapshot == ProjectSnapshot &&
tracker.FilePath == "c:\\SomeFilePath.cshtml" &&
tracker.IsSupportedProject == isSupportedProject);
return documentTracker;
@ -32,7 +54,7 @@ namespace Microsoft.VisualStudio.Editor.Razor
var parser = new DefaultVisualStudioRazorParser(
Dispatcher,
CreateDocumentTracker(),
Mock.Of<RazorProjectEngineFactoryService>(),
ProjectEngineFactory,
new DefaultErrorReporter(),
Mock.Of<VisualStudioCompletionBroker>());
parser.Dispose();
@ -48,7 +70,7 @@ namespace Microsoft.VisualStudio.Editor.Razor
var parser = new DefaultVisualStudioRazorParser(
Dispatcher,
CreateDocumentTracker(),
Mock.Of<RazorProjectEngineFactoryService>(),
ProjectEngineFactory,
new DefaultErrorReporter(),
Mock.Of<VisualStudioCompletionBroker>());
parser.Dispose();
@ -64,7 +86,7 @@ namespace Microsoft.VisualStudio.Editor.Razor
var parser = new DefaultVisualStudioRazorParser(
Dispatcher,
CreateDocumentTracker(),
Mock.Of<RazorProjectEngineFactoryService>(),
ProjectEngineFactory,
new DefaultErrorReporter(),
Mock.Of<VisualStudioCompletionBroker>());
parser.Dispose();
@ -80,7 +102,7 @@ namespace Microsoft.VisualStudio.Editor.Razor
using (var parser = new DefaultVisualStudioRazorParser(
Dispatcher,
CreateDocumentTracker(),
Mock.Of<RazorProjectEngineFactoryService>(),
ProjectEngineFactory,
new DefaultErrorReporter(),
Mock.Of<VisualStudioCompletionBroker>()))
{
@ -108,7 +130,7 @@ namespace Microsoft.VisualStudio.Editor.Razor
using (var parser = new DefaultVisualStudioRazorParser(
Dispatcher,
documentTracker,
Mock.Of<RazorProjectEngineFactoryService>(),
ProjectEngineFactory,
new DefaultErrorReporter(),
Mock.Of<VisualStudioCompletionBroker>()))
{
@ -139,7 +161,7 @@ namespace Microsoft.VisualStudio.Editor.Razor
using (var parser = new DefaultVisualStudioRazorParser(
Dispatcher,
CreateDocumentTracker(),
Mock.Of<RazorProjectEngineFactoryService>(),
ProjectEngineFactory,
new DefaultErrorReporter(),
Mock.Of<VisualStudioCompletionBroker>())
{
@ -169,7 +191,7 @@ namespace Microsoft.VisualStudio.Editor.Razor
using (var parser = new DefaultVisualStudioRazorParser(
Dispatcher,
CreateDocumentTracker(),
Mock.Of<RazorProjectEngineFactoryService>(),
ProjectEngineFactory,
new DefaultErrorReporter(),
Mock.Of<VisualStudioCompletionBroker>())
{
@ -198,7 +220,7 @@ namespace Microsoft.VisualStudio.Editor.Razor
using (var parser = new DefaultVisualStudioRazorParser(
Dispatcher,
CreateDocumentTracker(),
Mock.Of<RazorProjectEngineFactoryService>(),
ProjectEngineFactory,
new DefaultErrorReporter(),
Mock.Of<VisualStudioCompletionBroker>()))
{
@ -222,7 +244,7 @@ namespace Microsoft.VisualStudio.Editor.Razor
using (var parser = new DefaultVisualStudioRazorParser(
Dispatcher,
documentTracker,
Mock.Of<RazorProjectEngineFactoryService>(),
ProjectEngineFactory,
new DefaultErrorReporter(),
Mock.Of<VisualStudioCompletionBroker>()))
{
@ -242,7 +264,7 @@ namespace Microsoft.VisualStudio.Editor.Razor
using (var parser = new DefaultVisualStudioRazorParser(
Dispatcher,
CreateDocumentTracker(isSupportedProject: true),
Mock.Of<RazorProjectEngineFactoryService>(),
ProjectEngineFactory,
new DefaultErrorReporter(),
Mock.Of<VisualStudioCompletionBroker>()))
{
@ -261,7 +283,7 @@ namespace Microsoft.VisualStudio.Editor.Razor
using (var parser = new DefaultVisualStudioRazorParser(
Dispatcher,
CreateDocumentTracker(isSupportedProject: false),
Mock.Of<RazorProjectEngineFactoryService>(),
ProjectEngineFactory,
new DefaultErrorReporter(),
Mock.Of<VisualStudioCompletionBroker>()))
{

View File

@ -5,6 +5,12 @@
<PreserveCompilationContext>true</PreserveCompilationContext>
</PropertyGroup>
<ItemGroup>
<Compile Include="..\Microsoft.CodeAnalysis.Razor.Workspaces.Test\Shared\**\*.cs">
<Link>Shared\%(RecursiveDir)%(FileName)%(Extension)</Link>
</Compile>
</ItemGroup>
<ItemGroup>
<None Include="xunit.runner.json" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>

View File

@ -12,12 +12,18 @@ 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 class BackgroundDocumentGeneratorTest : ForegroundDispatcherTestBase
{
public ProjectSnapshotWorkerQueueTest()
public BackgroundDocumentGeneratorTest()
{
HostProject1 = new HostProject("Test1.csproj", FallbackRazorConfiguration.MVC_1_0);
HostProject2 = new HostProject("Test2.csproj", FallbackRazorConfiguration.MVC_1_0);
Documents = new HostDocument[]
{
new HostDocument("c:\\Test1\\Index.cshtml", "Index.cshtml"),
new HostDocument("c:\\Test1\\Components\\Counter.cshtml", "Components\\Counter.cshtml"),
};
HostProject1 = new HostProject("c:\\Test1\\Test1.csproj", FallbackRazorConfiguration.MVC_1_0);
HostProject2 = new HostProject("c:\\Test2\\Test2.csproj", FallbackRazorConfiguration.MVC_1_0);
Workspace = TestWorkspace.Create();
@ -31,19 +37,21 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
"Test1",
"Test1",
LanguageNames.CSharp,
"Test1.csproj"))
"c:\\Test1\\Test1.csproj"))
.AddProject(ProjectInfo.Create(
projectId2,
VersionStamp.Default,
"Test2",
"Test2",
LanguageNames.CSharp,
"Test2.csproj")); ;
"c:\\Test2\\Test2.csproj")); ;
WorkspaceProject1 = solution.GetProject(projectId1);
WorkspaceProject2 = solution.GetProject(projectId2);
}
private HostDocument[] Documents { get; }
private HostProject HostProject1 { get; }
private HostProject HostProject2 { get; }
@ -63,28 +71,31 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
projectManager.HostProjectAdded(HostProject2);
projectManager.WorkspaceProjectAdded(WorkspaceProject1);
projectManager.WorkspaceProjectAdded(WorkspaceProject2);
projectManager.DocumentAdded(HostProject1, Documents[0]);
projectManager.DocumentAdded(HostProject1, Documents[1]);
var projectWorker = new TestProjectSnapshotWorker();
var project = projectManager.GetLoadedProject(HostProject1.FilePath);
var queue = new ProjectSnapshotWorkerQueue(Dispatcher, projectManager, projectWorker)
var queue = new BackgroundDocumentGenerator(Dispatcher)
{
Delay = TimeSpan.FromMilliseconds(1),
BlockBackgroundWorkStart = new ManualResetEventSlim(initialState: false),
NotifyBackgroundWorkFinish = new ManualResetEventSlim(initialState: false),
NotifyForegroundWorkFinish = new ManualResetEventSlim(initialState: false),
NotifyBackgroundWorkStarting = new ManualResetEventSlim(initialState: false),
BlockBackgroundWorkCompleting = new ManualResetEventSlim(initialState: false),
NotifyBackgroundWorkCompleted = new ManualResetEventSlim(initialState: false),
};
// Act & Assert
queue.Enqueue(projectManager.GetSnapshot(HostProject1).CreateUpdateContext());
queue.Enqueue(project, project.GetDocument(Documents[0].FilePath));
Assert.True(queue.IsScheduledOrRunning, "Queue should be scheduled during Enqueue");
Assert.True(queue.HasPendingNotifications, "Queue should have a notification created during Enqueue");
// Allow the background work to proceed.
queue.BlockBackgroundWorkStart.Set();
queue.BlockBackgroundWorkCompleting.Set();
// Get off the foreground thread and allow the updates to flow through.
await Task.Run(() => queue.NotifyForegroundWorkFinish.Wait(TimeSpan.FromSeconds(1)));
await Task.Run(() => queue.NotifyBackgroundWorkCompleted.Wait(TimeSpan.FromSeconds(1)));
Assert.False(queue.IsScheduledOrRunning, "Queue should not have restarted");
Assert.False(queue.HasPendingNotifications, "Queue should have processed all notifications");
@ -99,40 +110,42 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
projectManager.HostProjectAdded(HostProject2);
projectManager.WorkspaceProjectAdded(WorkspaceProject1);
projectManager.WorkspaceProjectAdded(WorkspaceProject2);
projectManager.DocumentAdded(HostProject1, Documents[0]);
projectManager.DocumentAdded(HostProject1, Documents[1]);
var projectWorker = new TestProjectSnapshotWorker();
var project = projectManager.GetLoadedProject(HostProject1.FilePath);
var queue = new ProjectSnapshotWorkerQueue(Dispatcher, projectManager, projectWorker)
var queue = new BackgroundDocumentGenerator(Dispatcher)
{
Delay = TimeSpan.FromMilliseconds(1),
BlockBackgroundWorkStart = new ManualResetEventSlim(initialState: false),
NotifyBackgroundWorkFinish = new ManualResetEventSlim(initialState: false),
NotifyForegroundWorkFinish = new ManualResetEventSlim(initialState: false),
NotifyBackgroundWorkStarting = new ManualResetEventSlim(initialState: false),
BlockBackgroundWorkCompleting = new ManualResetEventSlim(initialState: false),
NotifyBackgroundWorkCompleted = new ManualResetEventSlim(initialState: false),
};
// Act & Assert
queue.Enqueue(projectManager.GetSnapshot(HostProject1).CreateUpdateContext());
queue.Enqueue(project, project.GetDocument(Documents[0].FilePath));
Assert.True(queue.IsScheduledOrRunning, "Queue should be scheduled during Enqueue");
Assert.True(queue.HasPendingNotifications, "Queue should have a notification created during Enqueue");
// Allow the background work to proceed.
// Allow the background work to start.
queue.BlockBackgroundWorkStart.Set();
queue.NotifyBackgroundWorkFinish.Wait(); // Block the foreground thread so we can queue another notification.
await Task.Run(() => queue.NotifyBackgroundWorkStarting.Wait(TimeSpan.FromSeconds(1)));
Assert.True(queue.IsScheduledOrRunning, "Worker should be processing now");
Assert.False(queue.HasPendingNotifications, "Worker should have taken all notifications");
queue.Enqueue(projectManager.GetSnapshot(HostProject2).CreateUpdateContext());
queue.Enqueue(project, project.GetDocument(Documents[1].FilePath));
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)));
// Allow work to complete, which should restart the timer.
queue.BlockBackgroundWorkCompleting.Set();
queue.NotifyBackgroundWorkFinish.Reset();
queue.NotifyForegroundWorkFinish.Reset();
await Task.Run(() => queue.NotifyBackgroundWorkCompleted.Wait(TimeSpan.FromSeconds(1)));
queue.NotifyBackgroundWorkCompleted.Reset();
// It should start running again right away.
Assert.True(queue.IsScheduledOrRunning, "Queue should be scheduled during Enqueue");
@ -141,51 +154,11 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
// 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)));
queue.BlockBackgroundWorkCompleting.Set();
await Task.Run(() => queue.NotifyBackgroundWorkCompleted.Wait(TimeSpan.FromSeconds(1)));
Assert.False(queue.IsScheduledOrRunning, "Queue should not have restarted");
Assert.False(queue.HasPendingNotifications, "Queue should have processed all notifications");
}
private class TestProjectSnapshotManager : DefaultProjectSnapshotManager
{
public TestProjectSnapshotManager(ForegroundDispatcher foregroundDispatcher, Workspace workspace)
: base(foregroundDispatcher, Mock.Of<ErrorReporter>(), new TestProjectSnapshotWorker(), Enumerable.Empty<ProjectSnapshotChangeTrigger>(), workspace)
{
}
public DefaultProjectSnapshot GetSnapshot(HostProject hostProject)
{
return Projects.Cast<DefaultProjectSnapshot>().FirstOrDefault(s => s.FilePath == hostProject.FilePath);
}
public DefaultProjectSnapshot GetSnapshot(Project workspaceProject)
{
return Projects.Cast<DefaultProjectSnapshot>().FirstOrDefault(s => s.FilePath == workspaceProject.FilePath);
}
protected override void NotifyListeners(ProjectChangeEventArgs e)
{
}
protected override void NotifyBackgroundWorker(ProjectSnapshotUpdateContext context)
{
Assert.NotNull(context.HostProject);
Assert.NotNull(context.WorkspaceProject);
}
}
private class TestProjectSnapshotWorker : ProjectSnapshotWorker
{
public TestProjectSnapshotWorker()
{
}
public override Task ProcessUpdateAsync(ProjectSnapshotUpdateContext update, CancellationToken cancellationToken = default(CancellationToken))
{
return Task.CompletedTask;
}
}
}
}

View File

@ -9,6 +9,12 @@
<GenerateBindingRedirectsOutputType>true</GenerateBindingRedirectsOutputType>
</PropertyGroup>
<ItemGroup>
<Compile Include="..\Microsoft.CodeAnalysis.Razor.Workspaces.Test\Shared\**\*.cs">
<Link>Shared\%(RecursiveDir)%(FileName)%(Extension)</Link>
</Compile>
</ItemGroup>
<ItemGroup>
<None Include="xunit.runner.json" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>

View File

@ -45,12 +45,12 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor
ErrorReporter = new DefaultErrorReporter();
ProjectManager = new TestProjectSnapshotManager(Workspace);
EngineFactory = new DefaultProjectEngineFactoryService(ProjectManager, FallbackFactory, CustomFactories);
EngineFactory = new DefaultProjectSnapshotProjectEngineFactory(FallbackFactory, CustomFactories);
}
private ErrorReporter ErrorReporter { get; }
private RazorProjectEngineFactoryService EngineFactory { get; }
private ProjectSnapshotProjectEngineFactory EngineFactory { get; }
private Lazy<IProjectEngineFactory, ICustomProjectEngineFactoryMetadata>[] CustomFactories { get; }
@ -72,7 +72,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor
// Arrange
ProjectManager.HostProjectAdded(HostProject_For_2_0);
var project = ProjectManager.GetProjectWithFilePath("Test.csproj");
var project = ProjectManager.GetLoadedProject("Test.csproj");
var resolver = new TestTagHelperResolver(EngineFactory, ErrorReporter, Workspace);
@ -89,7 +89,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor
ProjectManager.HostProjectAdded(HostProject_For_2_0);
ProjectManager.WorkspaceProjectAdded(WorkspaceProject);
var project = ProjectManager.GetProjectWithFilePath("Test.csproj");
var project = ProjectManager.GetLoadedProject("Test.csproj");
var resolver = new TestTagHelperResolver(EngineFactory, ErrorReporter, Workspace)
{
@ -105,7 +105,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor
var result = await resolver.GetTagHelpersAsync(project);
// Assert
Assert.Same(TagHelperResolutionResult.Empty, result);
Assert.Same(TagHelperResolutionResult.Empty, result);
}
[Fact]
@ -115,7 +115,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor
ProjectManager.HostProjectAdded(HostProject_For_NonSerializableConfiguration);
ProjectManager.WorkspaceProjectAdded(WorkspaceProject);
var project = ProjectManager.GetProjectWithFilePath("Test.csproj");
var project = ProjectManager.GetLoadedProject("Test.csproj");
var resolver = new TestTagHelperResolver(EngineFactory, ErrorReporter, Workspace)
{
@ -136,8 +136,8 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor
private class TestTagHelperResolver : OOPTagHelperResolver
{
public TestTagHelperResolver(RazorProjectEngineFactoryService engineFactory, ErrorReporter errorReporter, Workspace workspace)
: base(engineFactory, errorReporter, workspace)
public TestTagHelperResolver(ProjectSnapshotProjectEngineFactory factory, ErrorReporter errorReporter, Workspace workspace)
: base(factory, errorReporter, workspace)
{
}
@ -163,15 +163,10 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor
: base(
Mock.Of<ForegroundDispatcher>(),
Mock.Of<ErrorReporter>(),
Mock.Of<ProjectSnapshotWorker>(),
Enumerable.Empty<ProjectSnapshotChangeTrigger>(),
workspace)
{
}
protected override void NotifyBackgroundWorker(ProjectSnapshotUpdateContext context)
{
}
}
}
}

View File

@ -1,8 +1,12 @@
// 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.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.CodeAnalysis.Host;
using Moq;
using Xunit;
@ -12,9 +16,31 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
{
public DefaultProjectSnapshotManagerTest()
{
HostProject = new HostProject("Test.csproj", FallbackRazorConfiguration.MVC_2_0);
TagHelperResolver = new TestTagHelperResolver();
Workspace = TestWorkspace.Create();
HostServices = TestServices.Create(
new IWorkspaceService[]
{
new TestProjectSnapshotProjectEngineFactory(),
},
new ILanguageService[]
{
TagHelperResolver,
});
Documents = new HostDocument[]
{
new HostDocument("c:\\MyProject\\File.cshtml", "File.cshtml"),
new HostDocument("c:\\MyProject\\Index.cshtml", "Index.cshtml"),
// linked file
new HostDocument("c:\\SomeOtherProject\\Index.cshtml", "Pages\\Index.cshtml"),
};
HostProject = new HostProject("c:\\MyProject\\Test.csproj", FallbackRazorConfiguration.MVC_2_0);
HostProjectWithConfigurationChange = new HostProject("c:\\MyProject\\Test.csproj", FallbackRazorConfiguration.MVC_1_0);
Workspace = TestWorkspace.Create(HostServices);
ProjectManager = new TestProjectSnapshotManager(Dispatcher, Enumerable.Empty<ProjectSnapshotChangeTrigger>(), Workspace);
var projectId = ProjectId.CreateNewId("Test");
@ -24,7 +50,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
"Test",
"Test",
LanguageNames.CSharp,
"Test.csproj"));
"c:\\MyProject\\Test.csproj"));
WorkspaceProject = solution.GetProject(projectId);
var vbProjectId = ProjectId.CreateNewId("VB");
@ -54,12 +80,19 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
"Test (Different TFM)",
"Test",
LanguageNames.CSharp,
"Test.csproj"));
"c:\\MyProject\\Test.csproj"));
WorkspaceProjectWithDifferentTfm = solution.GetProject(projectIdWithDifferentTfm);
SomeTagHelpers = TagHelperResolver.TagHelpers;
SomeTagHelpers.Add(TagHelperDescriptorBuilder.Create("Test1", "TestAssembly").Build());
}
private HostDocument[] Documents { get; }
private HostProject HostProject { get; }
private HostProject HostProjectWithConfigurationChange { get; }
private Project WorkspaceProject { get; }
private Project WorkspaceProjectWithDifferentTfm { get; }
@ -68,30 +101,203 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
private Project VBWorkspaceProject { get; }
private TestTagHelperResolver TagHelperResolver { get; }
private TestProjectSnapshotManager ProjectManager { get; }
private HostServices HostServices { get; }
private Workspace Workspace { get; }
private IList<TagHelperDescriptor> SomeTagHelpers { get; }
[ForegroundFact]
public void HostProjectBuildComplete_FindsChangedWorkspaceProject_AndStartsBackgroundWorker()
public void DocumentAdded_AddsDocument()
{
// Arrange
Assert.True(Workspace.TryApplyChanges(WorkspaceProject.Solution));
ProjectManager.HostProjectAdded(HostProject);
var project = WorkspaceProject.WithAssemblyName("Test1"); // Simulate a project change
ProjectManager.WorkspaceProjectAdded(project);
ProjectManager.WorkspaceProjectAdded(WorkspaceProject);
ProjectManager.Reset();
// Act
ProjectManager.HostProjectBuildComplete(HostProject);
ProjectManager.DocumentAdded(HostProject, Documents[0]);
// Assert
var snapshot = ProjectManager.GetSnapshot(HostProject);
Assert.True(snapshot.IsDirty);
Assert.True(snapshot.IsInitialized);
Assert.Collection(snapshot.DocumentFilePaths, d => Assert.Equal(Documents[0].FilePath, d));
Assert.False(ProjectManager.ListenersNotified);
Assert.True(ProjectManager.WorkerStarted);
Assert.Equal(ProjectChangeKind.DocumentsChanged, ProjectManager.ListenersNotifiedOf);
}
[ForegroundFact]
public void DocumentAdded_IgnoresDuplicate()
{
// Arrange
ProjectManager.HostProjectAdded(HostProject);
ProjectManager.WorkspaceProjectAdded(WorkspaceProject);
ProjectManager.DocumentAdded(HostProject, Documents[0]);
ProjectManager.Reset();
// Act
ProjectManager.DocumentAdded(HostProject, Documents[0]);
// Assert
var snapshot = ProjectManager.GetSnapshot(HostProject);
Assert.Collection(snapshot.DocumentFilePaths, d => Assert.Equal(Documents[0].FilePath, d));
Assert.Null(ProjectManager.ListenersNotifiedOf);
}
[ForegroundFact]
public void DocumentAdded_IgnoresUnknownProject()
{
// Arrange
// Act
ProjectManager.DocumentAdded(HostProject, Documents[0]);
// Assert
var snapshot = ProjectManager.GetSnapshot(HostProject);
Assert.Null(snapshot);
}
[ForegroundFact]
public async Task DocumentAdded_CachesTagHelpers()
{
// Arrange
ProjectManager.HostProjectAdded(HostProject);
ProjectManager.WorkspaceProjectAdded(WorkspaceProject);
ProjectManager.Reset();
// Adding some computed state
var snapshot = ProjectManager.GetSnapshot(HostProject);
await snapshot.GetTagHelpersAsync();
// Act
ProjectManager.DocumentAdded(HostProject, Documents[0]);
// Assert
snapshot = ProjectManager.GetSnapshot(HostProject);
Assert.True(snapshot.TryGetTagHelpers(out var _));
}
[ForegroundFact]
public void DocumentAdded_CachesProjectEngine()
{
// Arrange
ProjectManager.HostProjectAdded(HostProject);
ProjectManager.Reset();
var snapshot = ProjectManager.GetSnapshot(HostProject);
var projectEngine = snapshot.GetProjectEngine();
// Act
ProjectManager.DocumentAdded(HostProject, Documents[0]);
// Assert
snapshot = ProjectManager.GetSnapshot(HostProject);
Assert.Same(projectEngine, snapshot.GetProjectEngine());
}
[ForegroundFact]
public void DocumentRemoved_RemovesDocument()
{
// Arrange
ProjectManager.HostProjectAdded(HostProject);
ProjectManager.WorkspaceProjectAdded(WorkspaceProject);
ProjectManager.DocumentAdded(HostProject, Documents[0]);
ProjectManager.DocumentAdded(HostProject, Documents[1]);
ProjectManager.DocumentAdded(HostProject, Documents[2]);
ProjectManager.Reset();
// Act
ProjectManager.DocumentRemoved(HostProject, Documents[1]);
// Assert
var snapshot = ProjectManager.GetSnapshot(HostProject);
Assert.Collection(
snapshot.DocumentFilePaths,
d => Assert.Equal(Documents[0].FilePath, d),
d => Assert.Equal(Documents[2].FilePath, d));
Assert.Equal(ProjectChangeKind.DocumentsChanged, ProjectManager.ListenersNotifiedOf);
}
[ForegroundFact]
public void DocumentRemoved_IgnoresNotFoundDocument()
{
// Arrange
ProjectManager.HostProjectAdded(HostProject);
ProjectManager.WorkspaceProjectAdded(WorkspaceProject);
ProjectManager.Reset();
// Act
ProjectManager.DocumentRemoved(HostProject, Documents[0]);
// Assert
var snapshot = ProjectManager.GetSnapshot(HostProject);
Assert.Empty(snapshot.DocumentFilePaths);
Assert.Null(ProjectManager.ListenersNotifiedOf);
}
[ForegroundFact]
public void DocumentRemoved_IgnoresUnknownProject()
{
// Arrange
// Act
ProjectManager.DocumentRemoved(HostProject, Documents[0]);
// Assert
var snapshot = ProjectManager.GetSnapshot(HostProject);
Assert.Null(snapshot);
}
[ForegroundFact]
public async Task DocumentRemoved_CachesTagHelpers()
{
// Arrange
ProjectManager.HostProjectAdded(HostProject);
ProjectManager.WorkspaceProjectAdded(WorkspaceProject);
ProjectManager.DocumentAdded(HostProject, Documents[0]);
ProjectManager.DocumentAdded(HostProject, Documents[1]);
ProjectManager.DocumentAdded(HostProject, Documents[2]);
ProjectManager.Reset();
// Adding some computed state
var snapshot = ProjectManager.GetSnapshot(HostProject);
await snapshot.GetTagHelpersAsync();
// Act
ProjectManager.DocumentRemoved(HostProject, Documents[1]);
// Assert
snapshot = ProjectManager.GetSnapshot(HostProject);
Assert.True(snapshot.TryGetTagHelpers(out var _));
}
[ForegroundFact]
public void DocumentRemoved_CachesProjectEngine()
{
// Arrange
ProjectManager.HostProjectAdded(HostProject);
ProjectManager.WorkspaceProjectAdded(WorkspaceProject);
ProjectManager.DocumentAdded(HostProject, Documents[0]);
ProjectManager.DocumentAdded(HostProject, Documents[1]);
ProjectManager.DocumentAdded(HostProject, Documents[2]);
ProjectManager.Reset();
var snapshot = ProjectManager.GetSnapshot(HostProject);
var projectEngine = snapshot.GetProjectEngine();
// Act
ProjectManager.DocumentRemoved(HostProject, Documents[1]);
// Assert
snapshot = ProjectManager.GetSnapshot(HostProject);
Assert.Same(projectEngine, snapshot.GetProjectEngine());
}
[ForegroundFact]
@ -104,15 +310,13 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
// Assert
var snapshot = ProjectManager.GetSnapshot(HostProject);
Assert.True(snapshot.IsDirty);
Assert.False(snapshot.IsInitialized);
Assert.True(ProjectManager.ListenersNotified);
Assert.False(ProjectManager.WorkerStarted);
Assert.Equal(ProjectChangeKind.ProjectAdded, ProjectManager.ListenersNotifiedOf);
}
[ForegroundFact]
public void HostProjectAdded_FindsWorkspaceProject_NotifiesListeners_AndStartsBackgroundWorker()
public void HostProjectAdded_FindsWorkspaceProject_NotifiesListeners()
{
// Arrange
Assert.True(Workspace.TryApplyChanges(WorkspaceProject.Solution));
@ -122,60 +326,85 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
// Assert
var snapshot = ProjectManager.GetSnapshot(HostProject);
Assert.True(snapshot.IsDirty);
Assert.True(snapshot.IsInitialized);
Assert.True(ProjectManager.ListenersNotified);
Assert.True(ProjectManager.WorkerStarted);
Assert.Equal(ProjectChangeKind.ProjectAdded, ProjectManager.ListenersNotifiedOf);
}
[ForegroundFact]
public void HostProjectChanged_WithoutWorkspaceProject_NotifiesListeners_AndDoesNotStartBackgroundWorker()
public void HostProjectChanged_ConfigurationChange_WithoutWorkspaceProject_NotifiesListeners()
{
// Arrange
ProjectManager.HostProjectAdded(HostProject);
ProjectManager.Reset();
var project = new HostProject(HostProject.FilePath, FallbackRazorConfiguration.MVC_1_0); // Simulate a project change
// Act
ProjectManager.HostProjectChanged(project);
ProjectManager.HostProjectChanged(HostProjectWithConfigurationChange);
// Assert
var snapshot = ProjectManager.GetSnapshot(HostProject);
Assert.True(snapshot.IsDirty);
var snapshot = ProjectManager.GetSnapshot(HostProjectWithConfigurationChange);
Assert.False(snapshot.IsInitialized);
Assert.True(ProjectManager.ListenersNotified);
Assert.False(ProjectManager.WorkerStarted);
Assert.Equal(ProjectChangeKind.ProjectChanged, ProjectManager.ListenersNotifiedOf);
}
[ForegroundFact]
public void HostProjectChanged_WithWorkspaceProject_RetainsComputedState_NotifiesListeners_AndStartsBackgroundWorker()
public void HostProjectChanged_ConfigurationChange_WithWorkspaceProject_NotifiesListeners()
{
// Arrange
ProjectManager.HostProjectAdded(HostProject);
ProjectManager.WorkspaceProjectAdded(WorkspaceProject);
ProjectManager.Reset();
// Adding some computed state
var snapshot = ProjectManager.GetSnapshot(HostProject);
var updateContext = snapshot.CreateUpdateContext();
ProjectManager.ProjectUpdated(updateContext);
ProjectManager.Reset();
var project = new HostProject(HostProject.FilePath, FallbackRazorConfiguration.MVC_1_0); // Simulate a project change
// Act
ProjectManager.HostProjectChanged(project);
ProjectManager.HostProjectChanged(HostProjectWithConfigurationChange);
// Assert
snapshot = ProjectManager.GetSnapshot(project);
Assert.True(snapshot.IsDirty);
var snapshot = ProjectManager.GetSnapshot(HostProjectWithConfigurationChange);
Assert.True(snapshot.IsInitialized);
Assert.True(ProjectManager.ListenersNotified);
Assert.True(ProjectManager.WorkerStarted);
Assert.Equal(ProjectChangeKind.ProjectChanged, ProjectManager.ListenersNotifiedOf);
}
[ForegroundFact]
public void HostProjectChanged_ConfigurationChange_DoesNotCacheProjectEngine()
{
// Arrange
ProjectManager.HostProjectAdded(HostProject);
ProjectManager.WorkspaceProjectAdded(WorkspaceProject);
ProjectManager.Reset();
var snapshot = ProjectManager.GetSnapshot(HostProject);
var projectEngine = snapshot.GetProjectEngine();
// Act
ProjectManager.HostProjectChanged(HostProjectWithConfigurationChange);
// Assert
snapshot = ProjectManager.GetSnapshot(HostProjectWithConfigurationChange);
Assert.NotSame(projectEngine, snapshot.GetProjectEngine());
}
[ForegroundFact]
public async Task HostProjectChanged_ConfigurationChange_DoesNotCacheComputedState()
{
// Arrange
ProjectManager.HostProjectAdded(HostProject);
ProjectManager.WorkspaceProjectAdded(WorkspaceProject);
ProjectManager.Reset();
var snapshot = ProjectManager.GetSnapshot(HostProject);
ProjectManager.Reset();
// Adding some computed state
await snapshot.GetTagHelpersAsync();
// Act
ProjectManager.HostProjectChanged(HostProjectWithConfigurationChange);
// Assert
snapshot = ProjectManager.GetSnapshot(HostProjectWithConfigurationChange);
Assert.False(snapshot.TryGetTagHelpers(out var _));
}
[ForegroundFact]
@ -189,8 +418,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
// Assert
Assert.Empty(ProjectManager.Projects);
Assert.False(ProjectManager.ListenersNotified);
Assert.False(ProjectManager.WorkerStarted);
Assert.Null(ProjectManager.ListenersNotifiedOf);
}
[ForegroundFact]
@ -206,277 +434,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
// Assert
Assert.Empty(ProjectManager.Projects);
Assert.True(ProjectManager.ListenersNotified);
Assert.False(ProjectManager.WorkerStarted);
}
[ForegroundFact]
public void ProjectUpdated_WithComputedState_IgnoresUnknownProject()
{
// Arrange
// Act
ProjectManager.ProjectUpdated(new ProjectSnapshotUpdateContext("Test", HostProject, WorkspaceProject, VersionStamp.Default));
// Assert
Assert.Empty(ProjectManager.Projects);
Assert.False(ProjectManager.ListenersNotified);
Assert.False(ProjectManager.WorkerStarted);
}
[ForegroundFact]
public void ProjectUpdated_WhenHostProjectChanged_MadeClean_NotifiesListeners_AndDoesNotStartBackgroundWorker()
{
// Arrange
ProjectManager.HostProjectAdded(HostProject);
ProjectManager.WorkspaceProjectAdded(WorkspaceProject);
ProjectManager.Reset();
var project = new HostProject(HostProject.FilePath, FallbackRazorConfiguration.MVC_1_0); // Simulate a project change
ProjectManager.HostProjectChanged(project);
ProjectManager.Reset();
// Generate the update
var snapshot = ProjectManager.GetSnapshot(HostProject);
var updateContext = snapshot.CreateUpdateContext();
// Act
ProjectManager.ProjectUpdated(updateContext);
// Assert
snapshot = ProjectManager.GetSnapshot(project);
Assert.False(snapshot.IsDirty);
Assert.True(ProjectManager.ListenersNotified);
Assert.False(ProjectManager.WorkerStarted);
}
[ForegroundFact]
public void ProjectUpdated_WhenWorkspaceProjectChanged_MadeClean_NotifiesListeners_AndDoesNotStartBackgroundWorker()
{
// Arrange
ProjectManager.HostProjectAdded(HostProject);
ProjectManager.WorkspaceProjectAdded(WorkspaceProject);
ProjectManager.Reset();
var project = WorkspaceProject.WithAssemblyName("Test1"); // Simulate a project change
ProjectManager.WorkspaceProjectChanged(project);
ProjectManager.Reset();
// Generate the update
var snapshot = ProjectManager.GetSnapshot(WorkspaceProject);
var updateContext = snapshot.CreateUpdateContext();
// Act
ProjectManager.ProjectUpdated(updateContext);
// Assert
snapshot = ProjectManager.GetSnapshot(project);
Assert.False(snapshot.IsDirty);
Assert.True(ProjectManager.ListenersNotified);
Assert.False(ProjectManager.WorkerStarted);
}
[ForegroundFact]
public void ProjectUpdated_WhenHostProjectChanged_StillDirty_WithSignificantChanges_NotifiesListeners_AndStartsBackgroundWorker()
{
// Arrange
ProjectManager.HostProjectAdded(HostProject);
ProjectManager.WorkspaceProjectAdded(WorkspaceProject);
ProjectManager.Reset();
// Generate the update
var snapshot = ProjectManager.GetSnapshot(HostProject);
var updateContext = snapshot.CreateUpdateContext();
var project = new HostProject(HostProject.FilePath, FallbackRazorConfiguration.MVC_1_0); // Simulate a project change
ProjectManager.HostProjectChanged(project);
ProjectManager.Reset();
// Act
ProjectManager.ProjectUpdated(updateContext);
// Assert
snapshot = ProjectManager.GetSnapshot(project);
Assert.True(snapshot.IsDirty);
Assert.True(ProjectManager.ListenersNotified);
Assert.True(ProjectManager.WorkerStarted);
}
[ForegroundFact]
public void WorkspaceProjectChanged_BackgroundUpdate_StillDirty_WithSignificantChanges_NotifiesListeners_AndStartsBackgroundWorker()
{
// Arrange
ProjectManager.HostProjectAdded(HostProject);
ProjectManager.WorkspaceProjectAdded(WorkspaceProject);
ProjectManager.Reset();
// Generate the update
var snapshot = ProjectManager.GetSnapshot(WorkspaceProject);
var updateContext = snapshot.CreateUpdateContext();
var project = WorkspaceProject.WithAssemblyName("Test1"); // Simulate a project change
ProjectManager.WorkspaceProjectChanged(project);
ProjectManager.Reset();
// Act
ProjectManager.ProjectUpdated(updateContext);
// Assert
snapshot = ProjectManager.GetSnapshot(project);
Assert.True(snapshot.IsDirty);
Assert.True(ProjectManager.ListenersNotified);
Assert.True(ProjectManager.WorkerStarted);
}
[Fact(Skip = "We no longer have any background-computed state")]
public void ProjectUpdated_WhenHostProjectChanged_StillDirty_WithoutSignificantChanges_DoesNotNotifyListeners_AndStartsBackgroundWorker()
{
// Arrange
ProjectManager.HostProjectAdded(HostProject);
ProjectManager.WorkspaceProjectAdded(WorkspaceProject);
ProjectManager.Reset();
// Generate an update based on the original state
var snapshot = ProjectManager.GetSnapshot(HostProject);
var updateContext = snapshot.CreateUpdateContext();
ProjectManager.ProjectUpdated(updateContext);
ProjectManager.Reset();
var project = new HostProject(HostProject.FilePath, FallbackRazorConfiguration.MVC_1_0); // Simulate a project change
ProjectManager.HostProjectChanged(project);
ProjectManager.Reset();
// Now start computing another update
snapshot = ProjectManager.GetSnapshot(HostProject);
updateContext = snapshot.CreateUpdateContext();
project = new HostProject(HostProject.FilePath, FallbackRazorConfiguration.MVC_1_1); // Simulate a project change
ProjectManager.HostProjectChanged(project);
ProjectManager.Reset();
// Act
ProjectManager.ProjectUpdated(updateContext); // Still dirty because the project changed while computing the update
// Assert
snapshot = ProjectManager.GetSnapshot(project);
Assert.True(snapshot.IsDirty);
Assert.False(ProjectManager.ListenersNotified);
Assert.True(ProjectManager.WorkerStarted);
}
[Fact(Skip = "We no longer have any background-computed state")]
public void ProjectUpdated_WhenWorkspaceProjectChanged_StillDirty_WithoutSignificantChanges_DoesNotNotifyListeners_AndStartsBackgroundWorker()
{
// Arrange
ProjectManager.HostProjectAdded(HostProject);
ProjectManager.WorkspaceProjectAdded(WorkspaceProject);
ProjectManager.Reset();
// Generate an update based on the original state
var snapshot = ProjectManager.GetSnapshot(HostProject);
var updateContext = snapshot.CreateUpdateContext();
ProjectManager.ProjectUpdated(updateContext);
ProjectManager.Reset();
var project = WorkspaceProject.WithAssemblyName("Test1"); // Simulate a project change
ProjectManager.WorkspaceProjectChanged(project);
ProjectManager.Reset();
// Now start computing another update
snapshot = ProjectManager.GetSnapshot(HostProject);
updateContext = snapshot.CreateUpdateContext();
project = project.WithAssemblyName("Test2"); // Simulate a project change
ProjectManager.WorkspaceProjectChanged(project);
ProjectManager.Reset();
// Act
ProjectManager.ProjectUpdated(updateContext); // Still dirty because the project changed while computing the update
// Assert
snapshot = ProjectManager.GetSnapshot(project);
Assert.True(snapshot.IsDirty);
Assert.False(ProjectManager.ListenersNotified);
Assert.True(ProjectManager.WorkerStarted);
}
[ForegroundFact]
public void ProjectUpdated_WhenHostProjectRemoved_DiscardsUpdate()
{
// Arrange
ProjectManager.HostProjectAdded(HostProject);
ProjectManager.WorkspaceProjectAdded(WorkspaceProject);
ProjectManager.Reset();
// Generate the update
var snapshot = ProjectManager.GetSnapshot(HostProject);
var updateContext = snapshot.CreateUpdateContext();
ProjectManager.HostProjectRemoved(HostProject);
ProjectManager.Reset();
// Act
ProjectManager.ProjectUpdated(updateContext);
// Assert
snapshot = ProjectManager.GetSnapshot(HostProject);
Assert.Null(snapshot);
}
[ForegroundFact]
public void ProjectUpdated_WhenWorkspaceProjectRemoved_DiscardsUpdate()
{
// Arrange
ProjectManager.HostProjectAdded(HostProject);
ProjectManager.WorkspaceProjectAdded(WorkspaceProject);
ProjectManager.Reset();
// Generate the update
var snapshot = ProjectManager.GetSnapshot(WorkspaceProject);
var updateContext = snapshot.CreateUpdateContext();
ProjectManager.WorkspaceProjectRemoved(WorkspaceProject);
ProjectManager.Reset();
// Act
ProjectManager.ProjectUpdated(updateContext);
// Assert
snapshot = ProjectManager.GetSnapshot(WorkspaceProject);
Assert.True(snapshot.IsDirty);
Assert.False(ProjectManager.ListenersNotified);
Assert.False(ProjectManager.WorkerStarted);
}
[ForegroundFact]
public void ProjectUpdated_BackgroundUpdate_MadeClean_WithSignificantChanges_NotifiesListeners_AndDoesNotStartBackgroundWorker()
{
// Arrange
ProjectManager.HostProjectAdded(HostProject);
ProjectManager.WorkspaceProjectAdded(WorkspaceProject);
ProjectManager.Reset();
// Generate the update
var snapshot = ProjectManager.GetSnapshot(HostProject);
var updateContext = snapshot.CreateUpdateContext();
// Act
ProjectManager.ProjectUpdated(updateContext);
// Assert
snapshot = ProjectManager.GetSnapshot(WorkspaceProject);
Assert.False(snapshot.IsDirty);
Assert.True(ProjectManager.ListenersNotified);
Assert.False(ProjectManager.WorkerStarted);
Assert.Equal(ProjectChangeKind.ProjectRemoved, ProjectManager.ListenersNotifiedOf);
}
[ForegroundFact]
@ -490,8 +448,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
// Assert
Assert.Empty(ProjectManager.Projects);
Assert.False(ProjectManager.ListenersNotified);
Assert.False(ProjectManager.WorkerStarted);
Assert.Null(ProjectManager.ListenersNotifiedOf);
}
[ForegroundFact]
@ -508,8 +465,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
var snapshot = ProjectManager.GetSnapshot(WorkspaceProject);
Assert.False(snapshot.IsInitialized);
Assert.False(ProjectManager.ListenersNotified);
Assert.False(ProjectManager.WorkerStarted);
Assert.Null(ProjectManager.ListenersNotifiedOf);
}
[ForegroundFact]
@ -527,8 +483,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
var snapshot = ProjectManager.GetSnapshot(WorkspaceProject);
Assert.Same(WorkspaceProject, snapshot.WorkspaceProject);
Assert.False(ProjectManager.ListenersNotified);
Assert.False(ProjectManager.WorkerStarted);
Assert.Null(ProjectManager.ListenersNotifiedOf);
}
[ForegroundFact]
@ -545,12 +500,11 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
var snapshot = ProjectManager.GetSnapshot(WorkspaceProject);
Assert.False(snapshot.IsInitialized);
Assert.False(ProjectManager.ListenersNotified);
Assert.False(ProjectManager.WorkerStarted);
Assert.Null(ProjectManager.ListenersNotifiedOf);
}
[ForegroundFact]
public void WorkspaceProjectAdded_WithHostProject_NotifiesListenters_AndStartsBackgroundWorker()
public void WorkspaceProjectAdded_WithHostProject_NotifiesListenters()
{
// Arrange
ProjectManager.HostProjectAdded(HostProject);
@ -561,11 +515,45 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
// Assert
var snapshot = ProjectManager.GetSnapshot(WorkspaceProject);
Assert.True(snapshot.IsDirty);
Assert.True(snapshot.IsInitialized);
Assert.True(ProjectManager.ListenersNotified);
Assert.True(ProjectManager.WorkerStarted);
Assert.Equal(ProjectChangeKind.ProjectChanged, ProjectManager.ListenersNotifiedOf);
}
[ForegroundFact]
public void WorkspaceProjectChanged_WithHostProject_NotifiesListenters()
{
// Arrange
ProjectManager.HostProjectAdded(HostProject);
ProjectManager.WorkspaceProjectAdded(WorkspaceProject);
ProjectManager.Reset();
// Act
ProjectManager.WorkspaceProjectChanged(WorkspaceProject.WithAssemblyName("Test1"));
// Assert
var snapshot = ProjectManager.GetSnapshot(WorkspaceProject);
Assert.True(snapshot.IsInitialized);
Assert.Equal(ProjectChangeKind.ProjectChanged, ProjectManager.ListenersNotifiedOf);
}
[ForegroundFact]
public void WorkspaceProjectChanged_WithHostProject_CanNoOp()
{
// Arrange
ProjectManager.HostProjectAdded(HostProject);
ProjectManager.WorkspaceProjectAdded(WorkspaceProject);
ProjectManager.Reset();
// Act
ProjectManager.WorkspaceProjectChanged(WorkspaceProject);
// Assert
var snapshot = ProjectManager.GetSnapshot(WorkspaceProject);
Assert.True(snapshot.IsInitialized);
Assert.Null(ProjectManager.ListenersNotifiedOf);
}
[ForegroundFact]
@ -583,8 +571,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
// Assert
Assert.Empty(ProjectManager.Projects);
Assert.False(ProjectManager.ListenersNotified);
Assert.False(ProjectManager.WorkerStarted);
Assert.Null(ProjectManager.ListenersNotifiedOf);
}
[ForegroundFact]
@ -604,11 +591,9 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
var snapshot = ProjectManager.GetSnapshot(WorkspaceProject);
Assert.False(snapshot.IsInitialized);
Assert.False(ProjectManager.ListenersNotified);
Assert.False(ProjectManager.WorkerStarted);
Assert.Null(ProjectManager.ListenersNotifiedOf);
}
[ForegroundFact]
public void WorkspaceProjectChanged_IgnoresProjectWithoutFilePath()
{
@ -626,8 +611,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
var snapshot = ProjectManager.GetSnapshot(WorkspaceProject);
Assert.False(snapshot.IsInitialized);
Assert.False(ProjectManager.ListenersNotified);
Assert.False(ProjectManager.WorkerStarted);
Assert.Null(ProjectManager.ListenersNotifiedOf);
}
[ForegroundFact]
@ -645,65 +629,46 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
var snapshot = ProjectManager.GetSnapshot(WorkspaceProject);
Assert.Same(WorkspaceProject, snapshot.WorkspaceProject);
Assert.False(ProjectManager.ListenersNotified);
Assert.False(ProjectManager.WorkerStarted);
Assert.Null(ProjectManager.ListenersNotifiedOf);
}
[ForegroundFact]
public void WorkspaceProjectChanged_MadeDirty_RetainsComputedState_NotifiesListeners_AndStartsBackgroundWorker()
public async Task WorkspaceProjectRemoved_DoesNotRemoveProject_RemovesTagHelpers()
{
// Arrange
ProjectManager.HostProjectAdded(HostProject);
ProjectManager.WorkspaceProjectAdded(WorkspaceProject);
ProjectManager.Reset();
// Generate the update
var snapshot = ProjectManager.GetSnapshot(HostProject);
var updateContext = snapshot.CreateUpdateContext();
ProjectManager.ProjectUpdated(updateContext);
ProjectManager.Reset();
var project = WorkspaceProject.WithAssemblyName("Test1"); // Simulate a project change
// Act
ProjectManager.WorkspaceProjectChanged(project);
// Assert
snapshot = ProjectManager.GetSnapshot(project);
Assert.True(snapshot.IsDirty);
Assert.False(ProjectManager.ListenersNotified);
Assert.True(ProjectManager.WorkerStarted);
}
[ForegroundFact]
public void WorkspaceProjectRemoved_WithHostProject_DoesNotRemoveProject()
{
// Arrange
ProjectManager.HostProjectAdded(HostProject);
ProjectManager.WorkspaceProjectAdded(WorkspaceProject);
ProjectManager.Reset();
// Adding some computed state
await snapshot.GetTagHelpersAsync();
// Act
ProjectManager.WorkspaceProjectRemoved(WorkspaceProject);
// Assert
var snapshot = ProjectManager.GetSnapshot(WorkspaceProject);
Assert.True(snapshot.IsDirty);
snapshot = ProjectManager.GetSnapshot(WorkspaceProject);
Assert.False(snapshot.IsInitialized);
Assert.False(snapshot.TryGetTagHelpers(out var _));
Assert.True(ProjectManager.ListenersNotified);
Assert.False(ProjectManager.WorkerStarted);
Assert.Equal(ProjectChangeKind.ProjectChanged, ProjectManager.ListenersNotifiedOf);
}
[ForegroundFact]
public void WorkspaceProjectRemoved_WithHostProject_FallsBackToSecondProject()
public async Task WorkspaceProjectRemoved_FallsBackToSecondProject()
{
// Arrange
ProjectManager.HostProjectAdded(HostProject);
ProjectManager.WorkspaceProjectAdded(WorkspaceProject);
ProjectManager.Reset();
var snapshot = ProjectManager.GetSnapshot(HostProject);
// Adding some computed state
await snapshot.GetTagHelpersAsync();
// Sets up a solution where the which has WorkspaceProjectWithDifferentTfm but not WorkspaceProject
// This will enable us to fall back and find the WorkspaceProjectWithDifferentTfm
Assert.True(Workspace.TryApplyChanges(WorkspaceProjectWithDifferentTfm.Solution));
@ -712,13 +677,12 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
ProjectManager.WorkspaceProjectRemoved(WorkspaceProject);
// Assert
var snapshot = ProjectManager.GetSnapshot(WorkspaceProject);
Assert.True(snapshot.IsDirty);
snapshot = ProjectManager.GetSnapshot(WorkspaceProject);
Assert.True(snapshot.IsInitialized);
Assert.Equal(WorkspaceProjectWithDifferentTfm.Id, snapshot.WorkspaceProject.Id);
Assert.False(snapshot.TryGetTagHelpers(out var _));
Assert.True(ProjectManager.ListenersNotified);
Assert.True(ProjectManager.WorkerStarted);
Assert.Equal(ProjectChangeKind.ProjectChanged, ProjectManager.ListenersNotifiedOf);
}
[ForegroundFact]
@ -736,8 +700,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
var snapshot = ProjectManager.GetSnapshot(WorkspaceProject);
Assert.Same(WorkspaceProject, snapshot.WorkspaceProject);
Assert.False(ProjectManager.ListenersNotified);
Assert.False(ProjectManager.WorkerStarted);
Assert.Null(ProjectManager.ListenersNotifiedOf);
}
[ForegroundFact]
@ -755,8 +718,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
var snapshot = ProjectManager.GetSnapshot(WorkspaceProject);
Assert.False(snapshot.IsInitialized);
Assert.False(ProjectManager.ListenersNotified);
Assert.False(ProjectManager.WorkerStarted);
Assert.Null(ProjectManager.ListenersNotifiedOf);
}
[ForegroundFact]
@ -774,8 +736,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
var snapshot = ProjectManager.GetSnapshot(WorkspaceProject);
Assert.False(snapshot.IsInitialized);
Assert.False(ProjectManager.ListenersNotified);
Assert.False(ProjectManager.WorkerStarted);
Assert.Null(ProjectManager.ListenersNotifiedOf);
}
[ForegroundFact]
@ -789,20 +750,17 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
// Assert
Assert.Empty(ProjectManager.Projects);
Assert.False(ProjectManager.ListenersNotified);
Assert.False(ProjectManager.WorkerStarted);
Assert.Null(ProjectManager.ListenersNotifiedOf);
}
private class TestProjectSnapshotManager : DefaultProjectSnapshotManager
{
public TestProjectSnapshotManager(ForegroundDispatcher dispatcher, IEnumerable<ProjectSnapshotChangeTrigger> triggers, Workspace workspace)
: base(dispatcher, Mock.Of<ErrorReporter>(), Mock.Of<ProjectSnapshotWorker>(), triggers, workspace)
: base(dispatcher, Mock.Of<ErrorReporter>(), triggers, workspace)
{
}
public bool ListenersNotified { get; private set; }
public bool WorkerStarted { get; private set; }
public ProjectChangeKind? ListenersNotifiedOf { get; private set; }
public DefaultProjectSnapshot GetSnapshot(HostProject hostProject)
{
@ -816,21 +774,12 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
public void Reset()
{
ListenersNotified = false;
WorkerStarted = false;
ListenersNotifiedOf = null;
}
protected override void NotifyListeners(ProjectChangeEventArgs e)
{
ListenersNotified = true;
}
protected override void NotifyBackgroundWorker(ProjectSnapshotUpdateContext context)
{
Assert.NotNull(context.HostProject);
Assert.NotNull(context.WorkspaceProject);
WorkerStarted = true;
ListenersNotifiedOf = e.Kind;
}
}
}

View File

@ -7,12 +7,11 @@ using System.Collections.Immutable;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.VisualStudio.LanguageServices.Razor;
using Microsoft.VisualStudio.ProjectSystem;
using Microsoft.VisualStudio.ProjectSystem.Properties;
using Moq;
using Xunit;
using ProjectStateItem = System.Collections.Generic.KeyValuePair<string, System.Collections.Immutable.IImmutableDictionary<string, string>>;
using ItemCollection = Microsoft.VisualStudio.ProjectSystem.ItemCollection;
namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
{
@ -22,8 +21,21 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
{
Workspace = new AdhocWorkspace();
ProjectManager = new TestProjectSnapshotManager(Dispatcher, Workspace);
ConfigurationItems = new ItemCollection(Rules.RazorConfiguration.SchemaName);
ExtensionItems = new ItemCollection(Rules.RazorExtension.SchemaName);
DocumentItems = new ItemCollection(Rules.RazorGenerateWithTargetPath.SchemaName);
RazorGeneralProperties = new PropertyCollection(Rules.RazorGeneral.SchemaName);
}
private ItemCollection ConfigurationItems { get; }
private ItemCollection ExtensionItems { get; }
private ItemCollection DocumentItems { get; }
private PropertyCollection RazorGeneralProperties { get; }
private TestProjectSnapshotManager ProjectManager { get; }
private Workspace Workspace { get; }
@ -285,11 +297,13 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
public void TryGetConfiguredExtensionNames_FailsIfNoExtensions()
{
// Arrange
var extensions = new Dictionary<string, string>().ToImmutableDictionary();
var configurationItem = new ProjectStateItem(Rules.RazorConfiguration.SchemaName, extensions);
var items = new ItemCollection(Rules.RazorConfiguration.SchemaName);
items.Item("Test");
var item = items.ToSnapshot().Items.Single();
// Act
var result = DefaultRazorProjectHost.TryGetConfiguredExtensionNames(configurationItem, out var configuredExtensionnames);
var result = DefaultRazorProjectHost.TryGetExtensionNames(item, out var configuredExtensionnames);
// Assert
Assert.False(result);
@ -300,14 +314,14 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
public void TryGetConfiguredExtensionNames_FailsIfEmptyExtensions()
{
// Arrange
var extensions = new Dictionary<string, string>()
{
[Rules.RazorConfiguration.ExtensionsProperty] = string.Empty
}.ToImmutableDictionary();
var configurationItem = new ProjectStateItem(Rules.RazorConfiguration.SchemaName, extensions);
var items = new ItemCollection(Rules.RazorConfiguration.SchemaName);
items.Item("Test");
items.Property("Test", Rules.RazorConfiguration.ExtensionsProperty, string.Empty);
var item = items.ToSnapshot().Items.Single();
// Act
var result = DefaultRazorProjectHost.TryGetConfiguredExtensionNames(configurationItem, out var configuredExtensionNames);
var result = DefaultRazorProjectHost.TryGetExtensionNames(item, out var configuredExtensionNames);
// Assert
Assert.False(result);
@ -319,14 +333,15 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
{
// Arrange
var expectedExtensionName = "SomeExtensionName";
var extensions = new Dictionary<string, string>()
{
[Rules.RazorConfiguration.ExtensionsProperty] = expectedExtensionName
}.ToImmutableDictionary();
var configurationItem = new ProjectStateItem(Rules.RazorConfiguration.SchemaName, extensions);
var items = new ItemCollection(Rules.RazorConfiguration.SchemaName);
items.Item("Test");
items.Property("Test", Rules.RazorConfiguration.ExtensionsProperty, "SomeExtensionName");
var item = items.ToSnapshot().Items.Single();
// Act
var result = DefaultRazorProjectHost.TryGetConfiguredExtensionNames(configurationItem, out var configuredExtensionNames);
var result = DefaultRazorProjectHost.TryGetExtensionNames(item, out var configuredExtensionNames);
// Assert
Assert.True(result);
@ -338,14 +353,14 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
public void TryGetConfiguredExtensionNames_SucceedsIfMultipleExtensions()
{
// Arrange
var extensions = new Dictionary<string, string>()
{
[Rules.RazorConfiguration.ExtensionsProperty] = "SomeExtensionName;SomeOtherExtensionName"
}.ToImmutableDictionary();
var configurationItem = new ProjectStateItem(Rules.RazorConfiguration.SchemaName, extensions);
var items = new ItemCollection(Rules.RazorConfiguration.SchemaName);
items.Item("Test");
items.Property("Test", Rules.RazorConfiguration.ExtensionsProperty, "SomeExtensionName;SomeOtherExtensionName");
var item = items.ToSnapshot().Items.Single();
// Act
var result = DefaultRazorProjectHost.TryGetConfiguredExtensionNames(configurationItem, out var configuredExtensionNames);
var result = DefaultRazorProjectHost.TryGetExtensionNames(item, out var configuredExtensionNames);
// Assert
Assert.True(result);
@ -597,7 +612,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
public async Task DefaultRazorProjectHost_ForegroundThread_CreateAndDispose_Succeeds()
{
// Arrange
var services = new TestProjectSystemServices("Test.csproj");
var services = new TestProjectSystemServices("c:\\MyProject\\Test.csproj");
var host = new DefaultRazorProjectHost(services, Workspace, ProjectManager);
// Act & Assert
@ -612,7 +627,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
public async Task DefaultRazorProjectHost_BackgroundThread_CreateAndDispose_Succeeds()
{
// Arrange
var services = new TestProjectSystemServices("Test.csproj");
var services = new TestProjectSystemServices("c:\\MyProject\\Test.csproj");
var host = new DefaultRazorProjectHost(services, Workspace, ProjectManager);
// Act & Assert
@ -623,41 +638,50 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
Assert.Empty(ProjectManager.Projects);
}
[ForegroundFact]
public async Task OnProjectChanged_ReadsProperties_InitializesProject()
[ForegroundFact] // This can happen if the .xaml files aren't included correctly.
public async Task DefaultRazorProjectHost_OnProjectChanged_NoRulesDefined()
{
// Arrange
var changes = new TestProjectChangeDescription[]
{
new TestProjectChangeDescription()
{
RuleName = Rules.RazorGeneral.SchemaName,
After = TestProjectRuleSnapshot.CreateProperties(Rules.RazorGeneral.SchemaName, new Dictionary<string, string>()
{
{ Rules.RazorGeneral.RazorLangVersionProperty, "2.1" },
{ Rules.RazorGeneral.RazorDefaultConfigurationProperty, "MVC-2.1" },
}),
},
new TestProjectChangeDescription()
{
RuleName = Rules.RazorConfiguration.SchemaName,
After = TestProjectRuleSnapshot.CreateItems(Rules.RazorConfiguration.SchemaName, new Dictionary<string, Dictionary<string, string>>()
{
{ "MVC-2.1", new Dictionary<string, string>() { { "Extensions", "MVC-2.1;Another-Thing" }, } },
})
},
new TestProjectChangeDescription()
{
RuleName = Rules.RazorExtension.SchemaName,
After = TestProjectRuleSnapshot.CreateItems(Rules.RazorExtension.SchemaName, new Dictionary<string, Dictionary<string, string>>()
{
{ "MVC-2.1", new Dictionary<string, string>(){ } },
{ "Another-Thing", new Dictionary<string, string>(){ } },
})
}
};
var services = new TestProjectSystemServices("Test.csproj");
var host = new DefaultRazorProjectHost(services, Workspace, ProjectManager);
// Act & Assert
await Task.Run(async () => await host.LoadAsync());
Assert.Empty(ProjectManager.Projects);
await Task.Run(async () => await host.OnProjectChanged(services.CreateUpdate(changes)));
Assert.Empty(ProjectManager.Projects);
}
[ForegroundFact]
public async Task OnProjectChanged_ReadsProperties_InitializesProject()
{
// Arrange
RazorGeneralProperties.Property(Rules.RazorGeneral.RazorLangVersionProperty, "2.1");
RazorGeneralProperties.Property(Rules.RazorGeneral.RazorDefaultConfigurationProperty, "MVC-2.1");
ConfigurationItems.Item("MVC-2.1");
ConfigurationItems.Property("MVC-2.1", Rules.RazorConfiguration.ExtensionsProperty, "MVC-2.1;Another-Thing");
ExtensionItems.Item("MVC-2.1");
ExtensionItems.Item("Another-Thing");
DocumentItems.Item("File.cshtml");
DocumentItems.Property("File.cshtml", Rules.RazorGenerateWithTargetPath.TargetPathProperty, "File.cshtml");
var changes = new TestProjectChangeDescription[]
{
RazorGeneralProperties.ToChange(),
ConfigurationItems.ToChange(),
ExtensionItems.ToChange(),
DocumentItems.ToChange(),
};
var services = new TestProjectSystemServices("c:\\MyProject\\Test.csproj");
var host = new DefaultRazorProjectHost(services, Workspace, ProjectManager);
@ -669,7 +693,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
// Assert
var snapshot = Assert.Single(ProjectManager.Projects);
Assert.Equal("Test.csproj", snapshot.FilePath);
Assert.Equal("c:\\MyProject\\Test.csproj", snapshot.FilePath);
Assert.Equal(RazorLanguageVersion.Version_2_1, snapshot.Configuration.LanguageVersion);
Assert.Equal("MVC-2.1", snapshot.Configuration.ConfigurationName);
@ -678,6 +702,15 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
e => Assert.Equal("MVC-2.1", e.ExtensionName),
e => Assert.Equal("Another-Thing", e.ExtensionName));
Assert.Collection(
snapshot.DocumentFilePaths.OrderBy(d => d),
d =>
{
var document = snapshot.GetDocument(d);
Assert.Equal("c:\\MyProject\\File.cshtml", document.FilePath);
Assert.Equal("File.cshtml", document.TargetPath);
});
await Task.Run(async () => await host.DisposeAsync());
Assert.Empty(ProjectManager.Projects);
}
@ -686,34 +719,24 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
public async Task OnProjectChanged_NoVersionFound_DoesNotIniatializeProject()
{
// Arrange
RazorGeneralProperties.Property(Rules.RazorGeneral.RazorLangVersionProperty, "");
RazorGeneralProperties.Property(Rules.RazorGeneral.RazorDefaultConfigurationProperty, "");
ConfigurationItems.Item("TestConfiguration");
ExtensionItems.Item("TestExtension");
DocumentItems.Item("File.cshtml");
var changes = new TestProjectChangeDescription[]
{
new TestProjectChangeDescription()
{
RuleName = Rules.RazorGeneral.SchemaName,
After = TestProjectRuleSnapshot.CreateProperties(Rules.RazorGeneral.SchemaName, new Dictionary<string, string>()
{
{ Rules.RazorGeneral.RazorLangVersionProperty, "" },
{ Rules.RazorGeneral.RazorDefaultConfigurationProperty, "" },
}),
},
new TestProjectChangeDescription()
{
RuleName = Rules.RazorConfiguration.SchemaName,
After = TestProjectRuleSnapshot.CreateItems(Rules.RazorConfiguration.SchemaName, new Dictionary<string, Dictionary<string, string>>()
{
})
},
new TestProjectChangeDescription()
{
RuleName = Rules.RazorExtension.SchemaName,
After = TestProjectRuleSnapshot.CreateItems(Rules.RazorExtension.SchemaName, new Dictionary<string, Dictionary<string, string>>()
{
})
}
RazorGeneralProperties.ToChange(),
ConfigurationItems.ToChange(),
ExtensionItems.ToChange(),
DocumentItems.ToChange(),
};
var services = new TestProjectSystemServices("Test.csproj");
var services = new TestProjectSystemServices("c:\\MyProject\\Test.csproj");
var host = new DefaultRazorProjectHost(services, Workspace, ProjectManager);
@ -734,37 +757,27 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
public async Task OnProjectChanged_UpdateProject_Succeeds()
{
// Arrange
RazorGeneralProperties.Property(Rules.RazorGeneral.RazorLangVersionProperty, "2.1");
RazorGeneralProperties.Property(Rules.RazorGeneral.RazorDefaultConfigurationProperty, "MVC-2.1");
ConfigurationItems.Item("MVC-2.1");
ConfigurationItems.Property("MVC-2.1", Rules.RazorConfiguration.ExtensionsProperty, "MVC-2.1;Another-Thing");
ExtensionItems.Item("MVC-2.1");
ExtensionItems.Item("Another-Thing");
DocumentItems.Item("File.cshtml");
DocumentItems.Property("File.cshtml", Rules.RazorGenerateWithTargetPath.TargetPathProperty, "File.cshtml");
var changes = new TestProjectChangeDescription[]
{
new TestProjectChangeDescription()
{
RuleName = Rules.RazorGeneral.SchemaName,
After = TestProjectRuleSnapshot.CreateProperties(Rules.RazorGeneral.SchemaName, new Dictionary<string, string>()
{
{ Rules.RazorGeneral.RazorLangVersionProperty, "2.1" },
{ Rules.RazorGeneral.RazorDefaultConfigurationProperty, "MVC-2.1" },
}),
},
new TestProjectChangeDescription()
{
RuleName = Rules.RazorConfiguration.SchemaName,
After = TestProjectRuleSnapshot.CreateItems(Rules.RazorConfiguration.SchemaName, new Dictionary<string, Dictionary<string, string>>()
{
{ "MVC-2.1", new Dictionary<string, string>() { { "Extensions", "MVC-2.1;Another-Thing" }, } },
})
},
new TestProjectChangeDescription()
{
RuleName = Rules.RazorExtension.SchemaName,
After = TestProjectRuleSnapshot.CreateItems(Rules.RazorExtension.SchemaName, new Dictionary<string, Dictionary<string, string>>()
{
{ "MVC-2.1", new Dictionary<string, string>(){ } },
{ "Another-Thing", new Dictionary<string, string>(){ } },
})
}
RazorGeneralProperties.ToChange(),
ConfigurationItems.ToChange(),
ExtensionItems.ToChange(),
DocumentItems.ToChange(),
};
var services = new TestProjectSystemServices("Test.csproj");
var services = new TestProjectSystemServices("c:\\MyProject\\Test.csproj");
var host = new DefaultRazorProjectHost(services, Workspace, ProjectManager);
@ -776,7 +789,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
// Assert - 1
var snapshot = Assert.Single(ProjectManager.Projects);
Assert.Equal("Test.csproj", snapshot.FilePath);
Assert.Equal("c:\\MyProject\\Test.csproj", snapshot.FilePath);
Assert.Equal(RazorLanguageVersion.Version_2_1, snapshot.Configuration.LanguageVersion);
Assert.Equal("MVC-2.1", snapshot.Configuration.ConfigurationName);
@ -785,17 +798,39 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
e => Assert.Equal("MVC-2.1", e.ExtensionName),
e => Assert.Equal("Another-Thing", e.ExtensionName));
Assert.Collection(
snapshot.DocumentFilePaths.OrderBy(d => d),
d =>
{
var document = snapshot.GetDocument(d);
Assert.Equal("c:\\MyProject\\File.cshtml", document.FilePath);
Assert.Equal("File.cshtml", document.TargetPath);
});
// Act - 2
changes[0].After.SetProperty(Rules.RazorGeneral.RazorLangVersionProperty, "2.0");
changes[0].After.SetProperty(Rules.RazorGeneral.RazorDefaultConfigurationProperty, "MVC-2.0");
changes[1].After.SetItem("MVC-2.0", new Dictionary<string, string>() { { "Extensions", "MVC-2.0;Another-Thing" }, });
changes[2].After.SetItem("MVC-2.0", new Dictionary<string, string>());
RazorGeneralProperties.Property(Rules.RazorGeneral.RazorLangVersionProperty, "2.0");
RazorGeneralProperties.Property(Rules.RazorGeneral.RazorDefaultConfigurationProperty, "MVC-2.0");
ConfigurationItems.RemoveItem("MVC-2.1");
ConfigurationItems.Item("MVC-2.0", new Dictionary<string, string>() { { "Extensions", "MVC-2.0;Another-Thing" }, });
ExtensionItems.Item("MVC-2.0");
DocumentItems.Item("c:\\AnotherProject\\AnotherFile.cshtml", new Dictionary<string, string>()
{
{ Rules.RazorGenerateWithTargetPath.TargetPathProperty, "Pages\\AnotherFile.cshtml" },
});
changes = new TestProjectChangeDescription[]
{
RazorGeneralProperties.ToChange(changes[0].After),
ConfigurationItems.ToChange(changes[1].After),
ExtensionItems.ToChange(changes[2].After),
DocumentItems.ToChange(changes[3].After),
};
await Task.Run(async () => await host.OnProjectChanged(services.CreateUpdate(changes)));
// Assert - 2
snapshot = Assert.Single(ProjectManager.Projects);
Assert.Equal("Test.csproj", snapshot.FilePath);
Assert.Equal("c:\\MyProject\\Test.csproj", snapshot.FilePath);
Assert.Equal(RazorLanguageVersion.Version_2_0, snapshot.Configuration.LanguageVersion);
Assert.Equal("MVC-2.0", snapshot.Configuration.ConfigurationName);
@ -804,6 +839,21 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
e => Assert.Equal("Another-Thing", e.ExtensionName),
e => Assert.Equal("MVC-2.0", e.ExtensionName));
Assert.Collection(
snapshot.DocumentFilePaths.OrderBy(d => d),
d =>
{
var document = snapshot.GetDocument(d);
Assert.Equal("c:\\AnotherProject\\AnotherFile.cshtml", document.FilePath);
Assert.Equal("Pages\\AnotherFile.cshtml", document.TargetPath);
},
d =>
{
var document = snapshot.GetDocument(d);
Assert.Equal("c:\\MyProject\\File.cshtml", document.FilePath);
Assert.Equal("File.cshtml", document.TargetPath);
});
await Task.Run(async () => await host.DisposeAsync());
Assert.Empty(ProjectManager.Projects);
}
@ -812,37 +862,27 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
public async Task OnProjectChanged_VersionRemoved_DeinitializesProject()
{
// Arrange
RazorGeneralProperties.Property(Rules.RazorGeneral.RazorLangVersionProperty, "2.1");
RazorGeneralProperties.Property(Rules.RazorGeneral.RazorDefaultConfigurationProperty, "MVC-2.1");
ConfigurationItems.Item("MVC-2.1");
ConfigurationItems.Property("MVC-2.1", Rules.RazorConfiguration.ExtensionsProperty, "MVC-2.1;Another-Thing");
ExtensionItems.Item("MVC-2.1");
ExtensionItems.Item("Another-Thing");
DocumentItems.Item("File.cshtml");
DocumentItems.Property("File.cshtml", Rules.RazorGenerateWithTargetPath.TargetPathProperty, "File.cshtml");
var changes = new TestProjectChangeDescription[]
{
new TestProjectChangeDescription()
{
RuleName = Rules.RazorGeneral.SchemaName,
After = TestProjectRuleSnapshot.CreateProperties(Rules.RazorGeneral.SchemaName, new Dictionary<string, string>()
{
{ Rules.RazorGeneral.RazorLangVersionProperty, "2.1" },
{ Rules.RazorGeneral.RazorDefaultConfigurationProperty, "MVC-2.1" },
}),
},
new TestProjectChangeDescription()
{
RuleName = Rules.RazorConfiguration.SchemaName,
After = TestProjectRuleSnapshot.CreateItems(Rules.RazorConfiguration.SchemaName, new Dictionary<string, Dictionary<string, string>>()
{
{ "MVC-2.1", new Dictionary<string, string>() { { "Extensions", "MVC-2.1;Another-Thing" }, } },
})
},
new TestProjectChangeDescription()
{
RuleName = Rules.RazorExtension.SchemaName,
After = TestProjectRuleSnapshot.CreateItems(Rules.RazorExtension.SchemaName, new Dictionary<string, Dictionary<string, string>>()
{
{ "MVC-2.1", new Dictionary<string, string>(){ } },
{ "Another-Thing", new Dictionary<string, string>(){ } },
})
}
RazorGeneralProperties.ToChange(),
ConfigurationItems.ToChange(),
ExtensionItems.ToChange(),
DocumentItems.ToChange(),
};
var services = new TestProjectSystemServices("Test.csproj");
var services = new TestProjectSystemServices("c:\\MyProject\\Test.csproj");
var host = new DefaultRazorProjectHost(services, Workspace, ProjectManager);
@ -854,7 +894,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
// Assert - 1
var snapshot = Assert.Single(ProjectManager.Projects);
Assert.Equal("Test.csproj", snapshot.FilePath);
Assert.Equal("c:\\MyProject\\Test.csproj", snapshot.FilePath);
Assert.Equal(RazorLanguageVersion.Version_2_1, snapshot.Configuration.LanguageVersion);
Assert.Equal("MVC-2.1", snapshot.Configuration.ConfigurationName);
@ -864,8 +904,16 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
e => Assert.Equal("Another-Thing", e.ExtensionName));
// Act - 2
changes[0].After.SetProperty(Rules.RazorGeneral.RazorLangVersionProperty, "");
changes[0].After.SetProperty(Rules.RazorGeneral.RazorDefaultConfigurationProperty, "");
RazorGeneralProperties.Property(Rules.RazorGeneral.RazorLangVersionProperty, "");
RazorGeneralProperties.Property(Rules.RazorGeneral.RazorDefaultConfigurationProperty, "");
changes = new TestProjectChangeDescription[]
{
RazorGeneralProperties.ToChange(changes[0].After),
ConfigurationItems.ToChange(changes[1].After),
ExtensionItems.ToChange(changes[2].After),
DocumentItems.ToChange(changes[3].After),
};
await Task.Run(async () => await host.OnProjectChanged(services.CreateUpdate(changes)));
@ -880,37 +928,27 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
public async Task OnProjectChanged_AfterDispose_IgnoresUpdate()
{
// Arrange
RazorGeneralProperties.Property(Rules.RazorGeneral.RazorLangVersionProperty, "2.1");
RazorGeneralProperties.Property(Rules.RazorGeneral.RazorDefaultConfigurationProperty, "MVC-2.1");
ConfigurationItems.Item("MVC-2.1");
ConfigurationItems.Property("MVC-2.1", Rules.RazorConfiguration.ExtensionsProperty, "MVC-2.1;Another-Thing");
ExtensionItems.Item("MVC-2.1");
ExtensionItems.Item("Another-Thing");
DocumentItems.Item("File.cshtml");
DocumentItems.Property("File.cshtml", Rules.RazorGenerateWithTargetPath.TargetPathProperty, "File.cshtml");
var changes = new TestProjectChangeDescription[]
{
new TestProjectChangeDescription()
{
RuleName = Rules.RazorGeneral.SchemaName,
After = TestProjectRuleSnapshot.CreateProperties(Rules.RazorGeneral.SchemaName, new Dictionary<string, string>()
{
{ Rules.RazorGeneral.RazorLangVersionProperty, "2.1" },
{ Rules.RazorGeneral.RazorDefaultConfigurationProperty, "MVC-2.1" },
}),
},
new TestProjectChangeDescription()
{
RuleName = Rules.RazorConfiguration.SchemaName,
After = TestProjectRuleSnapshot.CreateItems(Rules.RazorConfiguration.SchemaName, new Dictionary<string, Dictionary<string, string>>()
{
{ "MVC-2.1", new Dictionary<string, string>() { { "Extensions", "MVC-2.1;Another-Thing" }, } },
})
},
new TestProjectChangeDescription()
{
RuleName = Rules.RazorExtension.SchemaName,
After = TestProjectRuleSnapshot.CreateItems(Rules.RazorExtension.SchemaName, new Dictionary<string, Dictionary<string, string>>()
{
{ "MVC-2.1", new Dictionary<string, string>(){ } },
{ "Another-Thing", new Dictionary<string, string>(){ } },
})
}
RazorGeneralProperties.ToChange(),
ConfigurationItems.ToChange(),
ExtensionItems.ToChange(),
DocumentItems.ToChange(),
};
var services = new TestProjectSystemServices("Test.csproj");
var services = new TestProjectSystemServices("c:\\MyProject\\Test.csproj");
var host = new DefaultRazorProjectHost(services, Workspace, ProjectManager);
@ -922,7 +960,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
// Assert - 1
var snapshot = Assert.Single(ProjectManager.Projects);
Assert.Equal("Test.csproj", snapshot.FilePath);
Assert.Equal("c:\\MyProject\\Test.csproj", snapshot.FilePath);
Assert.Equal(RazorLanguageVersion.Version_2_1, snapshot.Configuration.LanguageVersion);
Assert.Equal("MVC-2.1", snapshot.Configuration.ConfigurationName);
@ -938,9 +976,17 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
Assert.Empty(ProjectManager.Projects);
// Act - 3
changes[0].After.SetProperty(Rules.RazorGeneral.RazorLangVersionProperty, "2.0");
changes[0].After.SetProperty(Rules.RazorGeneral.RazorDefaultConfigurationProperty, "MVC-2.0");
changes[1].After.SetItem("MVC-2.0", new Dictionary<string, string>() { { "Extensions", "MVC-2.0;Another-Thing" }, });
RazorGeneralProperties.Property(Rules.RazorGeneral.RazorLangVersionProperty, "2.0");
RazorGeneralProperties.Property(Rules.RazorGeneral.RazorDefaultConfigurationProperty, "MVC-2.0");
ConfigurationItems.Item("MVC-2.0", new Dictionary<string, string>() { { "Extensions", "MVC-2.0;Another-Thing" }, });
changes = new TestProjectChangeDescription[]
{
RazorGeneralProperties.ToChange(changes[0].After),
ConfigurationItems.ToChange(changes[1].After),
ExtensionItems.ToChange(changes[2].After),
DocumentItems.ToChange(changes[3].After),
};
await Task.Run(async () => await host.OnProjectChanged(services.CreateUpdate(changes)));
@ -952,37 +998,27 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
public async Task OnProjectRenamed_RemovesHostProject_CopiesConfiguration()
{
// Arrange
RazorGeneralProperties.Property(Rules.RazorGeneral.RazorLangVersionProperty, "2.1");
RazorGeneralProperties.Property(Rules.RazorGeneral.RazorDefaultConfigurationProperty, "MVC-2.1");
ConfigurationItems.Item("MVC-2.1");
ConfigurationItems.Property("MVC-2.1", Rules.RazorConfiguration.ExtensionsProperty, "MVC-2.1;Another-Thing");
ExtensionItems.Item("MVC-2.1");
ExtensionItems.Item("Another-Thing");
DocumentItems.Item("File.cshtml");
DocumentItems.Property("File.cshtml", Rules.RazorGenerateWithTargetPath.TargetPathProperty, "File.cshtml");
var changes = new TestProjectChangeDescription[]
{
new TestProjectChangeDescription()
{
RuleName = Rules.RazorGeneral.SchemaName,
After = TestProjectRuleSnapshot.CreateProperties(Rules.RazorGeneral.SchemaName, new Dictionary<string, string>()
{
{ Rules.RazorGeneral.RazorLangVersionProperty, "2.1" },
{ Rules.RazorGeneral.RazorDefaultConfigurationProperty, "MVC-2.1" },
}),
},
new TestProjectChangeDescription()
{
RuleName = Rules.RazorConfiguration.SchemaName,
After = TestProjectRuleSnapshot.CreateItems(Rules.RazorConfiguration.SchemaName, new Dictionary<string, Dictionary<string, string>>()
{
{ "MVC-2.1", new Dictionary<string, string>() { { "Extensions", "MVC-2.1;Another-Thing" }, } },
})
},
new TestProjectChangeDescription()
{
RuleName = Rules.RazorExtension.SchemaName,
After = TestProjectRuleSnapshot.CreateItems(Rules.RazorExtension.SchemaName, new Dictionary<string, Dictionary<string, string>>()
{
{ "MVC-2.1", new Dictionary<string, string>(){ } },
{ "Another-Thing", new Dictionary<string, string>(){ } },
})
}
RazorGeneralProperties.ToChange(),
ConfigurationItems.ToChange(),
ExtensionItems.ToChange(),
DocumentItems.ToChange(),
};
var services = new TestProjectSystemServices("Test.csproj");
var services = new TestProjectSystemServices("c:\\MyProject\\Test.csproj");
var host = new DefaultRazorProjectHost(services, Workspace, ProjectManager);
@ -994,16 +1030,16 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
// Assert - 1
var snapshot = Assert.Single(ProjectManager.Projects);
Assert.Equal("Test.csproj", snapshot.FilePath);
Assert.Equal("c:\\MyProject\\Test.csproj", snapshot.FilePath);
Assert.Same("MVC-2.1", snapshot.Configuration.ConfigurationName);
// Act - 2
services.UnconfiguredProject.FullPath = "Test2.csproj";
services.UnconfiguredProject.FullPath = "c:\\AnotherProject\\Test2.csproj";
await Task.Run(async () => await host.OnProjectRenamingAsync());
// Assert - 1
snapshot = Assert.Single(ProjectManager.Projects);
Assert.Equal("Test2.csproj", snapshot.FilePath);
Assert.Equal("c:\\AnotherProject\\Test2.csproj", snapshot.FilePath);
Assert.Same("MVC-2.1", snapshot.Configuration.ConfigurationName);
await Task.Run(async () => await host.DisposeAsync());
@ -1012,12 +1048,8 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
private class TestProjectSnapshotManager : DefaultProjectSnapshotManager
{
public TestProjectSnapshotManager(ForegroundDispatcher dispatcher, Workspace workspace)
: base(dispatcher, Mock.Of<ErrorReporter>(), Mock.Of<ProjectSnapshotWorker>(), Array.Empty<ProjectSnapshotChangeTrigger>(), workspace)
{
}
protected override void NotifyBackgroundWorker(ProjectSnapshotUpdateContext context)
public TestProjectSnapshotManager(ForegroundDispatcher dispatcher, Workspace workspace)
: base(dispatcher, Mock.Of<ErrorReporter>(), Array.Empty<ProjectSnapshotChangeTrigger>(), workspace)
{
}
}

View File

@ -4,7 +4,6 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.VisualStudio.LanguageServices.Razor;
using Microsoft.VisualStudio.ProjectSystem;
using Moq;
using Xunit;
@ -17,8 +16,12 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
{
Workspace = new AdhocWorkspace();
ProjectManager = new TestProjectSnapshotManager(Dispatcher, Workspace);
ReferenceItems = new ItemCollection(ManageProjectSystemSchema.ResolvedCompilationReference.SchemaName);
}
private ItemCollection ReferenceItems { get; }
private TestProjectSnapshotManager ProjectManager { get; }
private Workspace Workspace { get; }
@ -53,20 +56,37 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
Assert.Empty(ProjectManager.Projects);
}
[ForegroundFact]
public async Task OnProjectChanged_ReadsProperties_InitializesProject()
[ForegroundFact] // This can happen if the .xaml files aren't included correctly.
public async Task OnProjectChanged_NoRulesDefined()
{
// Arrange
var changes = new TestProjectChangeDescription[]
{
new TestProjectChangeDescription()
{
RuleName = ManageProjectSystemSchema.ResolvedCompilationReference.SchemaName,
After = TestProjectRuleSnapshot.CreateItems(ManageProjectSystemSchema.ResolvedCompilationReference.SchemaName, new Dictionary<string, Dictionary<string, string>>()
{
{ "c:\\nuget\\Microsoft.AspNetCore.Mvc.razor.dll", new Dictionary<string, string>() },
}),
},
};
var services = new TestProjectSystemServices("Test.csproj");
var host = new TestFallbackRazorProjectHost(services, Workspace, ProjectManager)
{
AssemblyVersion = new Version(2, 0),
};
// Act & Assert
await Task.Run(async () => await host.LoadAsync());
Assert.Empty(ProjectManager.Projects);
await Task.Run(async () => await host.OnProjectChanged(services.CreateUpdate(changes)));
Assert.Empty(ProjectManager.Projects);
}
[ForegroundFact]
public async Task OnProjectChanged_ReadsProperties_InitializesProject()
{
// Arrange
ReferenceItems.Item("c:\\nuget\\Microsoft.AspNetCore.Mvc.razor.dll");
var changes = new TestProjectChangeDescription[]
{
ReferenceItems.ToChange(),
};
var services = new TestProjectSystemServices("Test.csproj");
@ -97,14 +117,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
// Arrange
var changes = new TestProjectChangeDescription[]
{
new TestProjectChangeDescription()
{
RuleName = ManageProjectSystemSchema.ResolvedCompilationReference.SchemaName,
After = TestProjectRuleSnapshot.CreateItems(ManageProjectSystemSchema.ResolvedCompilationReference.SchemaName, new Dictionary<string, Dictionary<string, string>>()
{
}),
},
ReferenceItems.ToChange(),
};
var services = new TestProjectSystemServices("Test.csproj");
@ -127,16 +140,11 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
public async Task OnProjectChanged_AssemblyFoundButCannotReadVersion_DoesNotIniatializeProject()
{
// Arrange
ReferenceItems.Item("c:\\nuget\\Microsoft.AspNetCore.Mvc.razor.dll");
var changes = new TestProjectChangeDescription[]
{
new TestProjectChangeDescription()
{
RuleName = ManageProjectSystemSchema.ResolvedCompilationReference.SchemaName,
After = TestProjectRuleSnapshot.CreateItems(ManageProjectSystemSchema.ResolvedCompilationReference.SchemaName, new Dictionary<string, Dictionary<string, string>>()
{
{ "c:\\nuget\\Microsoft.AspNetCore.Mvc.razor.dll", new Dictionary<string, string>() },
}),
},
ReferenceItems.ToChange(),
};
var services = new TestProjectSystemServices("Test.csproj");
@ -160,16 +168,11 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
public async Task OnProjectChanged_UpdateProject_Succeeds()
{
// Arrange
ReferenceItems.Item("c:\\nuget\\Microsoft.AspNetCore.Mvc.razor.dll");
var changes = new TestProjectChangeDescription[]
{
new TestProjectChangeDescription()
{
RuleName = ManageProjectSystemSchema.ResolvedCompilationReference.SchemaName,
After = TestProjectRuleSnapshot.CreateItems(ManageProjectSystemSchema.ResolvedCompilationReference.SchemaName, new Dictionary<string, Dictionary<string, string>>()
{
{ "c:\\nuget\\Microsoft.AspNetCore.Mvc.razor.dll", new Dictionary<string, string>() },
}),
},
ReferenceItems.ToChange(),
};
var services = new TestProjectSystemServices("Test.csproj");
@ -207,16 +210,11 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
public async Task OnProjectChanged_VersionRemoved_DeinitializesProject()
{
// Arrange
ReferenceItems.Item("c:\\nuget\\Microsoft.AspNetCore.Mvc.razor.dll");
var changes = new TestProjectChangeDescription[]
{
new TestProjectChangeDescription()
{
RuleName = ManageProjectSystemSchema.ResolvedCompilationReference.SchemaName,
After = TestProjectRuleSnapshot.CreateItems(ManageProjectSystemSchema.ResolvedCompilationReference.SchemaName, new Dictionary<string, Dictionary<string, string>>()
{
{ "c:\\nuget\\Microsoft.AspNetCore.Mvc.razor.dll", new Dictionary<string, string>() },
}),
},
ReferenceItems.ToChange(),
};
var services = new TestProjectSystemServices("Test.csproj");
@ -252,16 +250,11 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
public async Task OnProjectChanged_AfterDispose_IgnoresUpdate()
{
// Arrange
ReferenceItems.Item("c:\\nuget\\Microsoft.AspNetCore.Mvc.razor.dll");
var changes = new TestProjectChangeDescription[]
{
new TestProjectChangeDescription()
{
RuleName = ManageProjectSystemSchema.ResolvedCompilationReference.SchemaName,
After = TestProjectRuleSnapshot.CreateItems(ManageProjectSystemSchema.ResolvedCompilationReference.SchemaName, new Dictionary<string, Dictionary<string, string>>()
{
{ "c:\\nuget\\Microsoft.AspNetCore.Mvc.razor.dll", new Dictionary<string, string>() },
}),
},
ReferenceItems.ToChange(),
};
var services = new TestProjectSystemServices("Test.csproj");
@ -300,16 +293,11 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
public async Task OnProjectRenamed_RemovesHostProject_CopiesConfiguration()
{
// Arrange
ReferenceItems.Item("c:\\nuget\\Microsoft.AspNetCore.Mvc.razor.dll");
var changes = new TestProjectChangeDescription[]
{
new TestProjectChangeDescription()
{
RuleName = ManageProjectSystemSchema.ResolvedCompilationReference.SchemaName,
After = TestProjectRuleSnapshot.CreateItems(ManageProjectSystemSchema.ResolvedCompilationReference.SchemaName, new Dictionary<string, Dictionary<string, string>>()
{
{ "c:\\nuget\\Microsoft.AspNetCore.Mvc.razor.dll", new Dictionary<string, string>() },
}),
},
ReferenceItems.ToChange(),
};
var services = new TestProjectSystemServices("Test.csproj");
@ -361,11 +349,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
private class TestProjectSnapshotManager : DefaultProjectSnapshotManager
{
public TestProjectSnapshotManager(ForegroundDispatcher dispatcher, Workspace workspace)
: base(dispatcher, Mock.Of<ErrorReporter>(), Mock.Of<ProjectSnapshotWorker>(), Array.Empty<ProjectSnapshotChangeTrigger>(), workspace)
{
}
protected override void NotifyBackgroundWorker(ProjectSnapshotUpdateContext context)
: base(dispatcher, Mock.Of<ErrorReporter>(), Array.Empty<ProjectSnapshotChangeTrigger>(), workspace)
{
}
}

View File

@ -0,0 +1,68 @@
// 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.Collections.Immutable;
using Microsoft.VisualStudio.ProjectSystem.Properties;
using Xunit;
namespace Microsoft.VisualStudio.ProjectSystem
{
internal class ItemCollection
{
private readonly string _ruleName;
private readonly Dictionary<string, Dictionary<string, string>> _items;
public ItemCollection(string ruleName)
{
_ruleName = ruleName;
_items = new Dictionary<string, Dictionary<string, string>>();
}
public void Item(string item)
{
Item(item, new Dictionary<string, string>());
}
public void Item(string item, Dictionary<string, string> properties)
{
_items[item] = properties;
}
public void RemoveItem(string item)
{
_items.Remove(item);
}
public void Property(string item, string key)
{
_items[item][key] = null;
}
public void Property(string item, string key, string value)
{
_items[item][key] = value;
}
public TestProjectRuleSnapshot ToSnapshot()
{
return TestProjectRuleSnapshot.CreateItems(_ruleName, _items);
}
public TestProjectChangeDescription ToChange()
{
return ToChange(new TestProjectRuleSnapshot(
_ruleName,
ImmutableDictionary<string, IImmutableDictionary<string, string>>.Empty,
ImmutableDictionary<string, string>.Empty,
ImmutableDictionary<NamedIdentity, IComparable>.Empty));
}
public TestProjectChangeDescription ToChange(IProjectRuleSnapshot before)
{
Assert.Equal(_ruleName, before.RuleName);
return new TestProjectChangeDescription(before, ToSnapshot());
}
}
}

View File

@ -0,0 +1,51 @@
// 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.Collections.Immutable;
using Microsoft.VisualStudio.ProjectSystem.Properties;
namespace Microsoft.VisualStudio.ProjectSystem
{
internal class PropertyCollection
{
private readonly string _ruleName;
private readonly Dictionary<string, string> _properties;
public PropertyCollection(string ruleName)
{
_ruleName = ruleName;
_properties = new Dictionary<string, string>();
}
public void Property(string key)
{
_properties[key] = null;
}
public void Property(string key, string value)
{
_properties[key] = value;
}
public TestProjectRuleSnapshot ToSnapshot()
{
return TestProjectRuleSnapshot.CreateProperties(_ruleName, _properties);
}
public TestProjectChangeDescription ToChange()
{
return ToChange(new TestProjectRuleSnapshot(
_ruleName,
ImmutableDictionary<string, IImmutableDictionary<string, string>>.Empty,
ImmutableDictionary<string, string>.Empty,
ImmutableDictionary<NamedIdentity, IComparable>.Empty));
}
public TestProjectChangeDescription ToChange(IProjectRuleSnapshot before)
{
return new TestProjectChangeDescription(before, ToSnapshot());
}
}
}

View File

@ -1,24 +1,99 @@
// 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.Collections.Immutable;
using Microsoft.VisualStudio.ProjectSystem.Properties;
namespace Microsoft.VisualStudio.ProjectSystem
{
internal class TestProjectChangeDescription : IProjectChangeDescription
{
public string RuleName { get; set; }
public TestProjectChangeDescription(IProjectRuleSnapshot before, IProjectRuleSnapshot after)
{
Before = before;
After = after;
public TestProjectRuleSnapshot Before { get; set; }
Difference = Diff.Create(before, after);
}
public IProjectChangeDiff Difference { get; set; }
public IProjectRuleSnapshot Before { get; }
public TestProjectRuleSnapshot After { get; set; }
public IProjectChangeDiff Difference { get; }
IProjectRuleSnapshot IProjectChangeDescription.Before => Before;
public IProjectRuleSnapshot After { get; }
IProjectChangeDiff IProjectChangeDescription.Difference => Difference;
private class Diff : IProjectChangeDiff
{
public static Diff Create(IProjectRuleSnapshot before, IProjectRuleSnapshot after)
{
var addedItems = new HashSet<string>(after.Items.Keys);
addedItems.ExceptWith(before.Items.Keys);
IProjectRuleSnapshot IProjectChangeDescription.After => After;
var removedItems = new HashSet<string>(before.Items.Keys);
removedItems.ExceptWith(after.Items.Keys);
// changed items must be present in both sets, but have different properties.
var changedItems = new HashSet<string>(before.Items.Keys);
changedItems.IntersectWith(after.Items.Keys);
changedItems.RemoveWhere(key =>
{
var x = before.Items[key];
var y = after.Items[key];
if (x.Count != y.Count)
{
return true;
}
foreach (var kvp in x)
{
if (!y.Contains(kvp))
{
return true;
}
}
return false;
});
var changedProperties = new HashSet<string>(before.Properties.Keys);
changedProperties.RemoveWhere(key =>
{
var x = before.Properties[key];
var y = after.Properties[key];
return object.Equals(x, y);
});
return new Diff()
{
AddedItems = addedItems.ToImmutableHashSet(),
RemovedItems = removedItems.ToImmutableHashSet(),
ChangedItems = changedItems.ToImmutableHashSet(),
// We ignore renamed items.
RenamedItems = ImmutableDictionary<string, string>.Empty,
ChangedProperties = changedProperties.ToImmutableHashSet(),
};
}
public IImmutableSet<string> AddedItems { get; private set; }
public IImmutableSet<string> RemovedItems { get; private set; }
public IImmutableSet<string> ChangedItems { get; private set; }
public IImmutableDictionary<string, string> RenamedItems { get; private set; }
public IImmutableSet<string> ChangedProperties { get; private set; }
public bool AnyChanges =>
AddedItems.Count > 0 ||
RemovedItems.Count > 0 ||
ChangedItems.Count > 0 ||
RenamedItems.Count > 0 ||
ChangedProperties.Count > 0;
}
}
}

View File

@ -40,21 +40,11 @@ namespace Microsoft.VisualStudio.ProjectSystem
DataSourceVersions = dataSourceVersions;
}
public void SetProperty(string key, string value)
{
Properties = Properties.SetItem(key, value);
}
public void SetItem(string key, Dictionary<string, string> values)
{
Items = Items.SetItem(key, values.ToImmutableDictionary());
}
public string RuleName { get; }
public IImmutableDictionary<string, IImmutableDictionary<string, string>> Items { get; set; }
public IImmutableDictionary<string, IImmutableDictionary<string, string>> Items { get; }
public IImmutableDictionary<string, string> Properties { get; set; }
public IImmutableDictionary<string, string> Properties { get; }
public IImmutableDictionary<NamedIdentity, IComparable> DataSourceVersions { get; }
}

View File

@ -82,7 +82,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
{
return new ProjectVersionedValue<IProjectSubscriptionUpdate>(
value: new ProjectSubscriptionUpdate(
projectChanges: descriptions.ToImmutableDictionary(d => d.RuleName, d => (IProjectChangeDescription)d),
projectChanges: descriptions.ToImmutableDictionary(d => d.After.RuleName, d => (IProjectChangeDescription)d),
projectConfiguration: ActiveConfiguredProject.ProjectConfiguration),
dataSourceVersions: ImmutableDictionary<NamedIdentity, IComparable>.Empty);
}

View File

@ -14,6 +14,41 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor
{
public class VsSolutionUpdatesProjectSnapshotChangeTriggerTest
{
public VsSolutionUpdatesProjectSnapshotChangeTriggerTest()
{
SomeProject = new HostProject("c:\\SomeProject\\SomeProject.csproj", FallbackRazorConfiguration.MVC_1_0);
SomeOtherProject = new HostProject("c:\\SomeOtherProject\\SomeOtherProject.csproj", FallbackRazorConfiguration.MVC_2_0);
Workspace = TestWorkspace.Create(w =>
{
SomeWorkspaceProject = w.AddProject(ProjectInfo.Create(
ProjectId.CreateNewId(),
VersionStamp.Create(),
"SomeProject",
"SomeProject",
LanguageNames.CSharp,
filePath: SomeProject.FilePath));
SomeOtherWorkspaceProject = w.AddProject(ProjectInfo.Create(
ProjectId.CreateNewId(),
VersionStamp.Create(),
"SomeOtherProject",
"SomeOtherProject",
LanguageNames.CSharp,
filePath: SomeOtherProject.FilePath));
});
}
private HostProject SomeProject { get; }
private HostProject SomeOtherProject { get; }
private Project SomeWorkspaceProject { get; set; }
private Project SomeOtherWorkspaceProject { get; set; }
private Workspace Workspace { get; }
[Fact]
public void Initialize_AttachesEventSink()
{
@ -38,10 +73,10 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor
}
[Fact]
public void UpdateProjectCfg_Done_KnownProject_Invokes_ProjectBuildComplete()
public void UpdateProjectCfg_Done_KnownProject_Invokes_WorkspaceProjectChanged()
{
// Arrange
var expectedProjectPath = "Path/To/Project";
var expectedProjectPath = SomeProject.FilePath;
uint cookie;
var buildManager = new Mock<IVsSolutionBuildManager>(MockBehavior.Strict);
@ -57,16 +92,19 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor
var projectSnapshots = new[]
{
Mock.Of<ProjectSnapshot>(p => p.FilePath == expectedProjectPath && p.HostProject == new HostProject(expectedProjectPath, RazorConfiguration.Default)),
Mock.Of<ProjectSnapshot>(p => p.FilePath == "Test2.csproj" && p.HostProject == new HostProject("Test2.csproj", RazorConfiguration.Default)),
new DefaultProjectSnapshot(new ProjectState(Workspace.Services, SomeProject, SomeWorkspaceProject)),
new DefaultProjectSnapshot(new ProjectState(Workspace.Services, SomeOtherProject, SomeOtherWorkspaceProject)),
};
var called = false;
var projectManager = new Mock<ProjectSnapshotManagerBase>();
projectManager.SetupGet(p => p.Projects).Returns(projectSnapshots);
projectManager.SetupGet(p => p.Workspace).Returns(Workspace);
projectManager
.Setup(p => p.HostProjectBuildComplete(It.IsAny<HostProject>()))
.Callback<HostProject>(c =>
.Setup(p => p.GetLoadedProject(expectedProjectPath))
.Returns(projectSnapshots[0]);
projectManager
.Setup(p => p.WorkspaceProjectChanged(It.IsAny<Project>()))
.Callback<Project>(c =>
{
called = true;
Assert.Equal(expectedProjectPath, c.FilePath);
@ -83,7 +121,50 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor
}
[Fact]
public void UpdateProjectCfg_Done_UnknownProject_DoesNotInvoke_ProjectBuildComplete()
public void UpdateProjectCfg_Done_WithoutWorkspaceProject_DoesNotInvoke_WorkspaceProjectChanged()
{
// Arrange
var expectedProjectPath = SomeProject.FilePath;
uint cookie;
var buildManager = new Mock<IVsSolutionBuildManager>(MockBehavior.Strict);
buildManager
.Setup(b => b.AdviseUpdateSolutionEvents(It.IsAny<VsSolutionUpdatesProjectSnapshotChangeTrigger>(), out cookie))
.Returns(VSConstants.S_OK);
var services = new Mock<IServiceProvider>();
services.Setup(s => s.GetService(It.Is<Type>(f => f == typeof(SVsSolutionBuildManager)))).Returns(buildManager.Object);
var projectService = new Mock<TextBufferProjectService>();
projectService.Setup(p => p.GetProjectPath(It.IsAny<IVsHierarchy>())).Returns(expectedProjectPath);
var projectSnapshots = new[]
{
new DefaultProjectSnapshot(new ProjectState(Workspace.Services, SomeProject, null)),
new DefaultProjectSnapshot(new ProjectState(Workspace.Services, SomeOtherProject, SomeOtherWorkspaceProject)),
};
var projectManager = new Mock<ProjectSnapshotManagerBase>();
projectManager.SetupGet(p => p.Workspace).Returns(Workspace);
projectManager
.Setup(p => p.GetLoadedProject(expectedProjectPath))
.Returns(projectSnapshots[0]);
projectManager
.Setup(p => p.WorkspaceProjectChanged(It.IsAny<Project>()))
.Callback<Project>(c =>
{
throw new InvalidOperationException("This should not be called.");
});
var trigger = new VsSolutionUpdatesProjectSnapshotChangeTrigger(services.Object, projectService.Object);
trigger.Initialize(projectManager.Object);
// Act & Assert - Does not throw
trigger.UpdateProjectCfg_Done(Mock.Of<IVsHierarchy>(), Mock.Of<IVsCfg>(), Mock.Of<IVsCfg>(), 0, 0, 0);
}
[Fact]
public void UpdateProjectCfg_Done_UnknownProject_DoesNotInvoke_WorkspaceProjectChanged()
{
// Arrange
var expectedProjectPath = "Path/To/Project";
@ -102,15 +183,18 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor
var projectSnapshots = new[]
{
Mock.Of<ProjectSnapshot>(p => p.FilePath == "Path/To/AnotherProject" && p.HostProject == new HostProject("Path/To/AnotherProject", RazorConfiguration.Default)),
Mock.Of<ProjectSnapshot>(p => p.FilePath == "Path/To/DifferenProject" && p.HostProject == new HostProject("Path/To/DifferenProject", RazorConfiguration.Default)),
new DefaultProjectSnapshot(new ProjectState(Workspace.Services, SomeProject, SomeWorkspaceProject)),
new DefaultProjectSnapshot(new ProjectState(Workspace.Services, SomeOtherProject, SomeOtherWorkspaceProject)),
};
var projectManager = new Mock<ProjectSnapshotManagerBase>();
projectManager.SetupGet(p => p.Projects).Returns(projectSnapshots);
projectManager.SetupGet(p => p.Workspace).Returns(Workspace);
projectManager
.Setup(p => p.HostProjectBuildComplete(It.IsAny<HostProject>()))
.Callback<HostProject>(c =>
.Setup(p => p.GetLoadedProject(expectedProjectPath))
.Returns((ProjectSnapshot)null);
projectManager
.Setup(p => p.WorkspaceProjectChanged(It.IsAny<Project>()))
.Callback<Project>(c =>
{
throw new InvalidOperationException("This should not be called.");
});

View File

@ -4,6 +4,12 @@
<TargetFramework>net461</TargetFramework>
</PropertyGroup>
<ItemGroup>
<Compile Include="..\Microsoft.CodeAnalysis.Razor.Workspaces.Test\Shared\**\*.cs">
<Link>Shared\%(RecursiveDir)%(FileName)%(Extension)</Link>
</Compile>
</ItemGroup>
<ItemGroup>
<None Include="xunit.runner.json" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>

View File

@ -10,29 +10,71 @@ using MonoDevelop.Projects;
using Moq;
using Xunit;
using Project = Microsoft.CodeAnalysis.Project;
using Workspace = Microsoft.CodeAnalysis.Workspace;
namespace Microsoft.VisualStudio.Mac.LanguageServices.Razor
{
public class ProjectBuildChangeTriggerTest : ForegroundDispatcherTestBase
{
public ProjectBuildChangeTriggerTest()
{
SomeProject = new HostProject("c:\\SomeProject\\SomeProject.csproj", FallbackRazorConfiguration.MVC_1_0);
SomeOtherProject = new HostProject("c:\\SomeOtherProject\\SomeOtherProject.csproj", FallbackRazorConfiguration.MVC_2_0);
Workspace = TestWorkspace.Create(w =>
{
SomeWorkspaceProject = w.AddProject(ProjectInfo.Create(
ProjectId.CreateNewId(),
VersionStamp.Create(),
"SomeProject",
"SomeProject",
LanguageNames.CSharp,
filePath: SomeProject.FilePath));
SomeOtherWorkspaceProject = w.AddProject(ProjectInfo.Create(
ProjectId.CreateNewId(),
VersionStamp.Create(),
"SomeOtherProject",
"SomeOtherProject",
LanguageNames.CSharp,
filePath: SomeOtherProject.FilePath));
});
}
private HostProject SomeProject { get; }
private HostProject SomeOtherProject { get; }
private Project SomeWorkspaceProject { get; set; }
private Project SomeOtherWorkspaceProject { get; set; }
private Workspace Workspace { get; }
[ForegroundFact]
public void ProjectOperations_EndBuild_Invokes_ProjectBuildComplete()
public void ProjectOperations_EndBuild_Invokes_WorkspaceProjectChanged()
{
// Arrange
var args = new BuildEventArgs(monitor: null, success: true);
var expectedProjectPath = "Path/To/Project";
var expectedProjectPath = SomeProject.FilePath;
var projectService = CreateProjectService(expectedProjectPath);
var args = new BuildEventArgs(monitor: null, success: true);
var projectSnapshots = new[]
{
Mock.Of<ProjectSnapshot>(p => p.FilePath == expectedProjectPath && p.HostProject == new HostProject(expectedProjectPath, RazorConfiguration.Default)),
Mock.Of<ProjectSnapshot>(p => p.FilePath == "Test2.csproj" && p.HostProject == new HostProject("Test2.csproj", RazorConfiguration.Default)),
new DefaultProjectSnapshot(new ProjectState(Workspace.Services, SomeProject, SomeWorkspaceProject)),
new DefaultProjectSnapshot(new ProjectState(Workspace.Services, SomeOtherProject, SomeOtherWorkspaceProject)),
};
var projectManager = new Mock<ProjectSnapshotManagerBase>(MockBehavior.Strict);
projectManager.SetupGet(p => p.Projects).Returns(projectSnapshots);
projectManager.SetupGet(p => p.Workspace).Returns(Workspace);
projectManager
.Setup(p => p.HostProjectBuildComplete(It.IsAny<HostProject>()))
.Callback<HostProject>(c => Assert.Equal(expectedProjectPath, c.FilePath));
.Setup(p => p.GetLoadedProject(SomeProject.FilePath))
.Returns(projectSnapshots[0]);
projectManager
.Setup(p => p.WorkspaceProjectChanged(It.IsAny<Project>()))
.Callback<Project>(c => Assert.Equal(expectedProjectPath, c.FilePath));
var trigger = new ProjectBuildChangeTrigger(Dispatcher, projectService, projectManager.Object);
// Act
@ -42,21 +84,56 @@ namespace Microsoft.VisualStudio.Mac.LanguageServices.Razor
projectManager.VerifyAll();
}
[ForegroundFact]
public void ProjectOperations_EndBuild_ProjectWithoutWorkspaceProject_Noops()
{
// Arrange
var projectService = CreateProjectService(SomeProject.FilePath);
var args = new BuildEventArgs(monitor: null, success: true);
var projectSnapshots = new[]
{
new DefaultProjectSnapshot(new ProjectState(Workspace.Services, SomeProject, null)),
new DefaultProjectSnapshot(new ProjectState(Workspace.Services, SomeOtherProject, SomeOtherWorkspaceProject)),
};
var projectManager = new Mock<ProjectSnapshotManagerBase>();
projectManager.SetupGet(p => p.Workspace).Returns(Workspace);
projectManager
.Setup(p => p.GetLoadedProject(SomeProject.FilePath))
.Returns(projectSnapshots[0]);
projectManager
.Setup(p => p.WorkspaceProjectChanged(It.IsAny<Project>()))
.Throws<InvalidOperationException>();
var trigger = new ProjectBuildChangeTrigger(Dispatcher, projectService, projectManager.Object);
// Act & Assert
trigger.ProjectOperations_EndBuild(null, args);
}
[ForegroundFact]
public void ProjectOperations_EndBuild_UntrackedProject_Noops()
{
// Arrange
var args = new BuildEventArgs(monitor: null, success: true);
var projectService = CreateProjectService("Path/To/Project");
var args = new BuildEventArgs(monitor: null, success: true);
var projectSnapshots = new[]
{
Mock.Of<ProjectSnapshot>(p => p.FilePath == "Path/To/AnotherProject" && p.HostProject == new HostProject("Path/To/AnotherProject", RazorConfiguration.Default)),
new DefaultProjectSnapshot(new ProjectState(Workspace.Services, SomeProject, null)),
new DefaultProjectSnapshot(new ProjectState(Workspace.Services, SomeOtherProject, SomeOtherWorkspaceProject)),
};
var projectManager = new Mock<ProjectSnapshotManagerBase>();
projectManager.SetupGet(p => p.Projects).Returns(projectSnapshots);
projectManager.SetupGet(p => p.Workspace).Returns(Workspace);
projectManager
.Setup(p => p.HostProjectBuildComplete(It.IsAny<HostProject>()))
.Setup(p => p.GetLoadedProject(SomeProject.FilePath))
.Returns(projectSnapshots[0]);
projectManager
.Setup(p => p.WorkspaceProjectChanged(It.IsAny<Project>()))
.Throws<InvalidOperationException>();
var trigger = new ProjectBuildChangeTrigger(Dispatcher, projectService, projectManager.Object);
// Act & Assert
@ -100,11 +177,5 @@ namespace Microsoft.VisualStudio.Mac.LanguageServices.Razor
projectService.Setup(p => p.IsSupportedProject(null)).Returns(true);
return projectService.Object;
}
private static AdhocWorkspace CreateProjectInWorkspace(AdhocWorkspace workspace, string name, string path)
{
workspace.AddProject(ProjectInfo.Create(ProjectId.CreateNewId(), new VersionStamp(), name, "TestAssembly", LanguageNames.CSharp, filePath: path));
return workspace;
}
}
}

View File

@ -73,13 +73,11 @@
<Compile Include="DocumentInfo\RazorDocumentInfoWindowControl.xaml.cs">
<DependentUpon>RazorDocumentInfoWindowControl.xaml</DependentUpon>
</Compile>
<Compile Include="RazorInfo\AssemblyViewModel.cs" />
<Compile Include="Behaviors\ItemSelectedBehavior.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Properties\BindingRedirectAttributes.cs" />
<Compile Include="RazorInfo\DirectiveViewModel.cs" />
<Compile Include="RazorInfo\DocumentInfoViewModel.cs" />
<Compile Include="RazorInfo\DocumentViewModel.cs" />
<Compile Include="RazorInfo\DirectiveDescriptorViewModel.cs" />
<Compile Include="RazorInfo\DocumentSnapshotViewModel.cs" />
<Compile Include="NotifyPropertyChanged.cs" />
<Compile Include="RazorInfo\ProjectSnapshotViewModel.cs" />
<Compile Include="RazorInfo\ProjectViewModel.cs" />

View File

@ -7,11 +7,11 @@ using Microsoft.AspNetCore.Razor.Language;
namespace Microsoft.VisualStudio.RazorExtension.RazorInfo
{
public class DirectiveViewModel : NotifyPropertyChanged
public class DirectiveDescriptorViewModel : NotifyPropertyChanged
{
private readonly DirectiveDescriptor _directive;
internal DirectiveViewModel(DirectiveDescriptor directive)
internal DirectiveDescriptorViewModel(DirectiveDescriptor directive)
{
_directive = directive;

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.
#if RAZOR_EXTENSION_DEVELOPER_MODE
using Microsoft.VisualStudio.LanguageServices.Razor;
namespace Microsoft.VisualStudio.RazorExtension.RazorInfo
{
public class DocumentInfoViewModel : NotifyPropertyChanged
{
private RazorEngineDocument _document;
internal DocumentInfoViewModel(RazorEngineDocument document)
{
_document = document;
}
public string Text => _document.Text;
}
}
#endif

View File

@ -2,22 +2,23 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
#if RAZOR_EXTENSION_DEVELOPER_MODE
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
namespace Microsoft.VisualStudio.RazorExtension.RazorInfo
{
public class AssemblyViewModel : NotifyPropertyChanged
public class DocumentSnapshotViewModel : NotifyPropertyChanged
{
private readonly ProjectExtensibilityAssembly _assembly;
internal AssemblyViewModel(ProjectExtensibilityAssembly assembly)
internal DocumentSnapshotViewModel(DocumentSnapshot document)
{
_assembly = assembly;
Name = _assembly.Identity.GetDisplayName();
Document = document;
}
public string Name { get; }
internal DocumentSnapshot Document { get; }
public string FilePath => Document.FilePath;
public string TargetPath => Document.TargetPath;
}
}
#endif

View File

@ -1,18 +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.
#if RAZOR_EXTENSION_DEVELOPER_MODE
namespace Microsoft.VisualStudio.RazorExtension.RazorInfo
{
public class DocumentViewModel : NotifyPropertyChanged
{
public DocumentViewModel(string filePath)
{
FilePath = filePath;
}
public string FilePath { get; }
}
}
#endif

View File

@ -3,16 +3,18 @@
#if RAZOR_EXTENSION_DEVELOPER_MODE
using System.Collections.ObjectModel;
using System.Windows;
namespace Microsoft.VisualStudio.RazorExtension.RazorInfo
{
public class ProjectInfoViewModel : NotifyPropertyChanged
{
private ObservableCollection<DirectiveViewModel> _directives;
private ObservableCollection<DocumentViewModel> _documents;
private ObservableCollection<DirectiveDescriptorViewModel> _directives;
private ObservableCollection<DocumentSnapshotViewModel> _documents;
private ObservableCollection<TagHelperViewModel> _tagHelpers;
private bool _tagHelpersLoading;
public ObservableCollection<DirectiveViewModel> Directives
public ObservableCollection<DirectiveDescriptorViewModel> Directives
{
get { return _directives; }
set
@ -22,7 +24,7 @@ namespace Microsoft.VisualStudio.RazorExtension.RazorInfo
}
}
public ObservableCollection<DocumentViewModel> Documents
public ObservableCollection<DocumentSnapshotViewModel> Documents
{
get { return _documents; }
set
@ -41,6 +43,20 @@ namespace Microsoft.VisualStudio.RazorExtension.RazorInfo
OnPropertyChanged();
}
}
public bool TagHelpersLoading
{
get { return _tagHelpersLoading; }
set
{
_tagHelpersLoading = value;
OnPropertyChanged();
OnPropertyChanged(nameof(TagHelperProgressVisibility));
}
}
public Visibility TagHelperProgressVisibility => TagHelpersLoading ? Visibility.Visible : Visibility.Hidden;
}
}
#endif

View File

@ -2,8 +2,10 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
#if RAZOR_EXTENSION_DEVELOPER_MODE
using System;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
@ -16,12 +18,9 @@ namespace Microsoft.VisualStudio.RazorExtension.RazorInfo
Project = project;
Id = project.WorkspaceProject?.Id;
Properties = new ObservableCollection<PropertyViewModel>()
{
new PropertyViewModel("Razor Language Version", project.Configuration?.LanguageVersion.ToString()),
new PropertyViewModel("Configuration Name", $"{project.Configuration?.ConfigurationName} ({project.Configuration?.GetType().Name ?? "unknown"})"),
new PropertyViewModel("Workspace Project", project.WorkspaceProject?.Name)
};
Properties = new ObservableCollection<PropertyViewModel>();
InitializeProperties();
}
internal ProjectSnapshot Project { get; }
@ -31,6 +30,26 @@ namespace Microsoft.VisualStudio.RazorExtension.RazorInfo
public ProjectId Id { get; }
public ObservableCollection<PropertyViewModel> Properties { get; }
private void InitializeProperties()
{
Properties.Clear();
Properties.Add(new PropertyViewModel("Language Version", Project.Configuration?.LanguageVersion.ToString()));
Properties.Add(new PropertyViewModel("Configuration", FormatConfiguration(Project)));
Properties.Add(new PropertyViewModel("Extensions", FormatExtensions(Project)));
Properties.Add(new PropertyViewModel("Workspace Project", Project.WorkspaceProject?.Name));
}
private static string FormatConfiguration(ProjectSnapshot project)
{
return $"{project.Configuration.ConfigurationName} ({project.Configuration.GetType().Name})";
}
private static string FormatExtensions(ProjectSnapshot project)
{
return $"{string.Join(", ", project.Configuration.Extensions.Select(e => e.ExtensionName))}";
}
}
}
#endif

Some files were not shown because too many files have changed in this diff Show More