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;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Microsoft.Extensions.Internal;
namespace Microsoft.AspNetCore.Razor.Language namespace Microsoft.AspNetCore.Razor.Language
{ {
public abstract class RazorConfiguration public abstract class RazorConfiguration : IEquatable<RazorConfiguration>
{ {
public static readonly RazorConfiguration Default = new DefaultRazorConfiguration( public static readonly RazorConfiguration Default = new DefaultRazorConfiguration(
RazorLanguageVersion.Latest, RazorLanguageVersion.Latest,
@ -43,6 +44,58 @@ namespace Microsoft.AspNetCore.Razor.Language
public abstract RazorLanguageVersion LanguageVersion { get; } 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 private class DefaultRazorConfiguration : RazorConfiguration
{ {
public DefaultRazorConfiguration( 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;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.Language;
namespace Microsoft.CodeAnalysis.Razor.ProjectSystem 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 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; State = state;
WorkspaceProject = workspaceProject; // Might be null
FilePath = hostProject.FilePath; _lock = new object();
Version = version ?? VersionStamp.Default; _documents = new Dictionary<string, DefaultDocumentSnapshot>(FilePathComparer.Instance);
} }
private DefaultProjectSnapshot(HostProject hostProject, DefaultProjectSnapshot other) public ProjectState State { get; }
{
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 override RazorConfiguration Configuration => HostProject.Configuration; 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 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>(); public override DocumentSnapshot GetDocument(string filePath)
// 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()
{ {
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);
} }
public DefaultProjectSnapshot WithHostProject(HostProject hostProject) return result;
{ }
if (hostProject == null)
{
throw new ArgumentNullException(nameof(hostProject));
} }
return new DefaultProjectSnapshot(hostProject, this); public override RazorProjectEngine GetProjectEngine()
{
return State.ProjectEngine.GetProjectEngine(this);
} }
public DefaultProjectSnapshot RemoveWorkspaceProject() public override Task<IReadOnlyList<TagHelperDescriptor>> GetTagHelpersAsync()
{ {
// We want to get rid of all of the computed state since it's not really valid. // IMPORTANT: Don't put more code here. We want this to return a cached task.
return new DefaultProjectSnapshot(HostProject, null, Version.GetNewerVersion()); return State.TagHelpers.GetTagHelperInitializationTask(this);
} }
public DefaultProjectSnapshot WithWorkspaceProject(Project workspaceProject) public override bool TryGetTagHelpers(out IReadOnlyList<TagHelperDescriptor> results)
{ {
if (workspaceProject == null) if (State.TagHelpers.IsResultAvailable)
{ {
throw new ArgumentNullException(nameof(workspaceProject)); results = State.TagHelpers.GetTagHelperInitializationTask(this).Result;
return true;
} }
return new DefaultProjectSnapshot(workspaceProject, this); results = null;
} return false;
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);
} }
} }
} }

View File

@ -3,7 +3,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.Linq; using System.Linq;
namespace Microsoft.CodeAnalysis.Razor.ProjectSystem namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
@ -31,15 +30,14 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
private readonly ErrorReporter _errorReporter; private readonly ErrorReporter _errorReporter;
private readonly ForegroundDispatcher _foregroundDispatcher; private readonly ForegroundDispatcher _foregroundDispatcher;
private readonly ProjectSnapshotChangeTrigger[] _triggers; 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( public DefaultProjectSnapshotManager(
ForegroundDispatcher foregroundDispatcher, ForegroundDispatcher foregroundDispatcher,
ErrorReporter errorReporter, ErrorReporter errorReporter,
ProjectSnapshotWorker worker,
IEnumerable<ProjectSnapshotChangeTrigger> triggers, IEnumerable<ProjectSnapshotChangeTrigger> triggers,
Workspace workspace) Workspace workspace)
{ {
@ -53,11 +51,6 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
throw new ArgumentNullException(nameof(errorReporter)); throw new ArgumentNullException(nameof(errorReporter));
} }
if (worker == null)
{
throw new ArgumentNullException(nameof(worker));
}
if (triggers == null) if (triggers == null)
{ {
throw new ArgumentNullException(nameof(triggers)); throw new ArgumentNullException(nameof(triggers));
@ -70,13 +63,10 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
_foregroundDispatcher = foregroundDispatcher; _foregroundDispatcher = foregroundDispatcher;
_errorReporter = errorReporter; _errorReporter = errorReporter;
_worker = worker;
_triggers = triggers.ToArray(); _triggers = triggers.ToArray();
Workspace = workspace; Workspace = workspace;
_projects = new Dictionary<string, DefaultProjectSnapshot>(FilePathComparer.Instance); _projects = new Dictionary<string, Entry>(FilePathComparer.Instance);
_workerQueue = new ProjectSnapshotWorkerQueue(_foregroundDispatcher, this, worker);
for (var i = 0; i < _triggers.Length; i++) for (var i = 0; i < _triggers.Length; i++)
{ {
@ -89,43 +79,109 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
get get
{ {
_foregroundDispatcher.AssertForegroundThread(); _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 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(); _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. entry.Snapshot = new DefaultProjectSnapshot(entry.State);
return;
} }
// This is an update to the project's computed values, so everything should be overwritten return entry.Snapshot;
var snapshot = original.WithComputedUpdate(update);
_projects[update.WorkspaceProject.FilePath] = snapshot;
if (snapshot.IsDirty)
{
// 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());
} }
if (!object.Equals(snapshot.ComputedVersion, original.ComputedVersion)) return null;
}
public override ProjectSnapshot GetOrCreateProject(string filePath)
{ {
NotifyListeners(new ProjectChangeEventArgs(snapshot, ProjectChangeKind.TagHelpersChanged)); 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))
{
_projects[hostProject.FilePath] = new Entry(state);
NotifyListeners(new ProjectChangeEventArgs(hostProject.FilePath, ProjectChangeKind.DocumentsChanged));
}
}
}
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))
{
_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. // So if possible find a WorkspaceProject.
var workspaceProject = GetWorkspaceProject(hostProject.FilePath); var workspaceProject = GetWorkspaceProject(hostProject.FilePath);
var snapshot = new DefaultProjectSnapshot(hostProject, workspaceProject); var state = new ProjectState(Workspace.Services, hostProject, workspaceProject);
_projects[hostProject.FilePath] = snapshot; _projects[hostProject.FilePath] = new Entry(state);
if (snapshot.IsInitialized && snapshot.IsDirty)
{
// Start computing background state if the project is fully initialized.
NotifyBackgroundWorker(snapshot.CreateUpdateContext());
}
// We need to notify listeners about every project add. // We need to notify listeners about every project add.
NotifyListeners(new ProjectChangeEventArgs(snapshot, ProjectChangeKind.Added)); NotifyListeners(new ProjectChangeEventArgs(hostProject.FilePath, ProjectChangeKind.ProjectAdded));
} }
public override void HostProjectChanged(HostProject hostProject) public override void HostProjectChanged(HostProject hostProject)
@ -171,22 +221,17 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
_foregroundDispatcher.AssertForegroundThread(); _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 var state = entry.State.WithHostProject(hostProject);
// underlying project is newer.
var snapshot = original.WithHostProject(hostProject);
_projects[hostProject.FilePath] = snapshot;
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. _projects[hostProject.FilePath] = new Entry(state);
NotifyBackgroundWorker(snapshot.CreateUpdateContext());
NotifyListeners(new ProjectChangeEventArgs(hostProject.FilePath, ProjectChangeKind.ProjectChanged));
} }
// Notify listeners right away because if the HostProject changes then it's likely that the Razor
// configuration changed.
NotifyListeners(new ProjectChangeEventArgs(snapshot, ProjectChangeKind.Changed));
} }
} }
@ -204,37 +249,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
_projects.Remove(hostProject.FilePath); _projects.Remove(hostProject.FilePath);
// We need to notify listeners about every project removal. // We need to notify listeners about every project removal.
NotifyListeners(new ProjectChangeEventArgs(snapshot, ProjectChangeKind.Removed)); NotifyListeners(new ProjectChangeEventArgs(hostProject.FilePath, ProjectChangeKind.ProjectRemoved));
}
}
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());
} }
} }
@ -254,25 +269,16 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
// The WorkspaceProject initialization never triggers a "Project Add" from out point of view, we // The WorkspaceProject initialization never triggers a "Project Add" from out point of view, we
// only care if the new WorkspaceProject matches an existing HostProject. // 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 // 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. // found one in the past just ignore this one.
if (original.WorkspaceProject == null) if (entry.State.WorkspaceProject == null)
{ {
var snapshot = original.WithWorkspaceProject(workspaceProject); var state = entry.State.WithWorkspaceProject(workspaceProject);
_projects[workspaceProject.FilePath] = snapshot; _projects[workspaceProject.FilePath] = new Entry(state);
if (snapshot.IsInitialized && snapshot.IsDirty) NotifyListeners(new ProjectChangeEventArgs(workspaceProject.FilePath, ProjectChangeKind.ProjectChanged));
{
// 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));
} }
} }
} }
@ -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 // 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. // in a single workspace project. Just use the one that showed up first.
if (_projects.TryGetValue(workspaceProject.FilePath, out var original) && if (_projects.TryGetValue(workspaceProject.FilePath, out var entry) &&
(original.WorkspaceProject == null || (entry.State.WorkspaceProject == null || entry.State.WorkspaceProject.Id == workspaceProject.Id))
original.WorkspaceProject.Id == workspaceProject.Id))
{ {
// Doing an update to the project should keep computed values, but mark the project as dirty if the var state = entry.State.WithWorkspaceProject(workspaceProject);
// underlying project is newer.
var snapshot = original.WithWorkspaceProject(workspaceProject);
_projects[workspaceProject.FilePath] = snapshot;
if (snapshot.IsInitialized && snapshot.IsDirty) // 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 _projects[workspaceProject.FilePath] = new Entry(state);
// need to trigger the background work to asynchronously compute the effect of the updates.
NotifyBackgroundWorker(snapshot.CreateUpdateContext());
}
if (snapshot.HaveTagHelpersChanged(original)) NotifyListeners(new ProjectChangeEventArgs(workspaceProject.FilePath, ProjectChangeKind.ProjectChanged));
{
NotifyListeners(new ProjectChangeEventArgs(snapshot, ProjectChangeKind.TagHelpersChanged));
} }
} }
} }
@ -330,16 +329,16 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
return; 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 // 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. // 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; return;
} }
DefaultProjectSnapshot snapshot; ProjectState state;
// So if the WorkspaceProject got removed, we should double check to make sure that there aren't others // 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. // 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) if (otherWorkspaceProject != null && otherWorkspaceProject.Id != workspaceProject.Id)
{ {
// OK there's another WorkspaceProject, use that. // OK there's another WorkspaceProject, use that.
// state = entry.State.WithWorkspaceProject(otherWorkspaceProject);
// Doing an update to the project should keep computed values, but mark the project as dirty if the _projects[otherWorkspaceProject.FilePath] = new Entry(state);
// underlying project is newer.
snapshot = original.WithWorkspaceProject(otherWorkspaceProject);
_projects[workspaceProject.FilePath] = snapshot;
if (snapshot.IsInitialized && snapshot.IsDirty) NotifyListeners(new ProjectChangeEventArgs(otherWorkspaceProject.FilePath, ProjectChangeKind.ProjectChanged));
}
else
{ {
// We don't need to notify listeners yet because we don't have any **new** computed state. However we do state = entry.State.WithWorkspaceProject(null);
// need to trigger the background work to asynchronously compute the effect of the updates. _projects[workspaceProject.FilePath] = new Entry(state);
NotifyBackgroundWorker(snapshot.CreateUpdateContext());
}
// Notify listeners of a change because it's a different WorkspaceProject.
NotifyListeners(new ProjectChangeEventArgs(snapshot, ProjectChangeKind.Changed));
return;
}
snapshot = original.RemoveWorkspaceProject();
_projects[workspaceProject.FilePath] = snapshot;
// Notify listeners of a change because we've removed computed state. // Notify listeners of a change because we've removed computed state.
NotifyListeners(new ProjectChangeEventArgs(snapshot, ProjectChangeKind.Changed)); NotifyListeners(new ProjectChangeEventArgs(workspaceProject.FilePath, ProjectChangeKind.ProjectChanged));
}
} }
} }
@ -401,8 +389,8 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
throw new ArgumentNullException(nameof(exception)); throw new ArgumentNullException(nameof(exception));
} }
var project = hostProject?.FilePath == null ? null : this.GetProjectWithFilePath(hostProject.FilePath); var snapshot = hostProject?.FilePath == null ? null : GetLoadedProject(hostProject.FilePath);
_errorReporter.ReportError(exception, project); _errorReporter.ReportError(exception, snapshot);
} }
public override void ReportError(Exception exception, Project workspaceProject) public override void ReportError(Exception exception, Project workspaceProject)
@ -440,14 +428,6 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
return null; 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 // virtual so it can be overridden in tests
protected virtual void NotifyListeners(ProjectChangeEventArgs e) protected virtual void NotifyListeners(ProjectChangeEventArgs e)
{ {
@ -459,5 +439,16 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
handler(this, e); handler(this, e);
} }
} }
private class Entry
{
public ProjectSnapshot Snapshot;
public readonly ProjectState State;
public Entry(ProjectState state)
{
State = state;
}
}
} }
} }

View File

@ -45,7 +45,6 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
return new DefaultProjectSnapshotManager( return new DefaultProjectSnapshotManager(
_foregroundDispatcher, _foregroundDispatcher,
languageServices.WorkspaceServices.GetRequiredService<ErrorReporter>(), languageServices.WorkspaceServices.GetRequiredService<ErrorReporter>(),
languageServices.GetRequiredService<ProjectSnapshotWorker>(),
_triggers, _triggers,
languageServices.WorkspaceServices.Workspace); 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 internal class ProjectChangeEventArgs : EventArgs
{ {
public ProjectChangeEventArgs(ProjectSnapshot project, ProjectChangeKind kind) public ProjectChangeEventArgs(string projectFilePath, ProjectChangeKind kind)
{ {
Project = project; ProjectFilePath = projectFilePath;
Kind = kind; Kind = kind;
} }
public ProjectSnapshot Project { get; } public string ProjectFilePath { get; }
public ProjectChangeKind Kind { get; } public ProjectChangeKind Kind { get; }
} }

View File

@ -5,9 +5,10 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
{ {
internal enum ProjectChangeKind internal enum ProjectChangeKind
{ {
Added, ProjectAdded,
Removed, ProjectRemoved,
Changed, ProjectChanged,
TagHelpersChanged, 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. // Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.Language;
namespace Microsoft.CodeAnalysis.Razor.ProjectSystem namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
@ -11,16 +11,22 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
{ {
public abstract RazorConfiguration Configuration { get; } public abstract RazorConfiguration Configuration { get; }
public abstract IEnumerable<string> DocumentFilePaths { get; }
public abstract string FilePath { get; } public abstract string FilePath { get; }
public abstract bool IsInitialized { get; } public abstract bool IsInitialized { get; }
public abstract IReadOnlyList<TagHelperDescriptor> TagHelpers { get; }
public abstract VersionStamp Version { get; } public abstract VersionStamp Version { get; }
public abstract Project WorkspaceProject { 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 event EventHandler<ProjectChangeEventArgs> Changed;
public abstract IReadOnlyList<ProjectSnapshot> Projects { get; } 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 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); public abstract void HostProjectAdded(HostProject hostProject);
@ -17,8 +19,6 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
public abstract void HostProjectRemoved(HostProject hostProject); public abstract void HostProjectRemoved(HostProject hostProject);
public abstract void HostProjectBuildComplete(HostProject hostProject);
public abstract void WorkspaceProjectAdded(Project workspaceProject); public abstract void WorkspaceProjectAdded(Project workspaceProject);
public abstract void WorkspaceProjectChanged(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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq;
using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.CodeAnalysis.Razor; using Microsoft.CodeAnalysis.Razor;
using Microsoft.CodeAnalysis.Razor.ProjectSystem; using Microsoft.CodeAnalysis.Razor.ProjectSystem;
@ -27,42 +23,5 @@ namespace Microsoft.CodeAnalysis.Remote.Razor
return await RazorServices.TagHelperResolver.GetTagHelpersAsync(project, factoryTypeName, cancellationToken); 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; FilePath = filePath;
Configuration = configuration; Configuration = configuration;
HostProject = new HostProject(filePath, configuration);
WorkspaceProject = workspaceProject; WorkspaceProject = workspaceProject;
TagHelpers = Array.Empty<TagHelperDescriptor>();
IsInitialized = true; IsInitialized = true;
Version = VersionStamp.Default; Version = VersionStamp.Default;
@ -58,6 +56,8 @@ namespace Microsoft.CodeAnalysis.Remote.Razor
public override RazorConfiguration Configuration { get; } public override RazorConfiguration Configuration { get; }
public override IEnumerable<string> DocumentFilePaths => Array.Empty<string>();
public override string FilePath { get; } public override string FilePath { get; }
public override bool IsInitialized { get; } public override bool IsInitialized { get; }
@ -66,9 +66,30 @@ namespace Microsoft.CodeAnalysis.Remote.Razor
public override Project WorkspaceProject { get; } 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 FileChangeTrackerFactory _fileChangeTrackerFactory;
private readonly ForegroundDispatcher _foregroundDispatcher; private readonly ForegroundDispatcher _foregroundDispatcher;
private readonly ErrorReporter _errorReporter; private readonly ErrorReporter _errorReporter;
private readonly RazorProjectEngineFactoryService _projectEngineFactoryService;
private readonly Dictionary<string, ImportTracker> _importTrackerCache; private readonly Dictionary<string, ImportTracker> _importTrackerCache;
public override event EventHandler<ImportChangedEventArgs> Changed; public override event EventHandler<ImportChangedEventArgs> Changed;
@ -24,8 +23,7 @@ namespace Microsoft.VisualStudio.Editor.Razor
public DefaultImportDocumentManager( public DefaultImportDocumentManager(
ForegroundDispatcher foregroundDispatcher, ForegroundDispatcher foregroundDispatcher,
ErrorReporter errorReporter, ErrorReporter errorReporter,
FileChangeTrackerFactory fileChangeTrackerFactory, FileChangeTrackerFactory fileChangeTrackerFactory)
RazorProjectEngineFactoryService projectEngineFactoryService)
{ {
if (foregroundDispatcher == null) if (foregroundDispatcher == null)
{ {
@ -42,15 +40,9 @@ namespace Microsoft.VisualStudio.Editor.Razor
throw new ArgumentNullException(nameof(fileChangeTrackerFactory)); throw new ArgumentNullException(nameof(fileChangeTrackerFactory));
} }
if (projectEngineFactoryService == null)
{
throw new ArgumentNullException(nameof(projectEngineFactoryService));
}
_foregroundDispatcher = foregroundDispatcher; _foregroundDispatcher = foregroundDispatcher;
_errorReporter = errorReporter; _errorReporter = errorReporter;
_fileChangeTrackerFactory = fileChangeTrackerFactory; _fileChangeTrackerFactory = fileChangeTrackerFactory;
_projectEngineFactoryService = projectEngineFactoryService;
_importTrackerCache = new Dictionary<string, ImportTracker>(StringComparer.OrdinalIgnoreCase); _importTrackerCache = new Dictionary<string, ImportTracker>(StringComparer.OrdinalIgnoreCase);
} }
@ -115,8 +107,7 @@ namespace Microsoft.VisualStudio.Editor.Razor
private IEnumerable<RazorProjectItem> GetImportItems(VisualStudioDocumentTracker tracker) private IEnumerable<RazorProjectItem> GetImportItems(VisualStudioDocumentTracker tracker)
{ {
var projectDirectory = Path.GetDirectoryName(tracker.ProjectPath); var projectEngine = tracker.ProjectSnapshot.GetProjectEngine();
var projectEngine = _projectEngineFactoryService.Create(projectDirectory, _ => { });
var trackerItem = projectEngine.FileSystem.GetItem(tracker.FilePath); var trackerItem = projectEngine.FileSystem.GetItem(tracker.FilePath);
var importFeature = projectEngine.ProjectFeatures.OfType<IImportProjectFeature>().FirstOrDefault(); 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 errorReporter = languageServices.WorkspaceServices.GetRequiredService<ErrorReporter>();
var fileChangeTrackerFactory = languageServices.GetRequiredService<FileChangeTrackerFactory>(); var fileChangeTrackerFactory = languageServices.GetRequiredService<FileChangeTrackerFactory>();
var projectEngineFactoryService = languageServices.GetRequiredService<RazorProjectEngineFactoryService>();
return new DefaultImportDocumentManager( return new DefaultImportDocumentManager(_foregroundDispatcher, errorReporter, fileChangeTrackerFactory);
_foregroundDispatcher,
errorReporter,
fileChangeTrackerFactory,
projectEngineFactoryService);
} }
} }
} }

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;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.CodeAnalysis.Razor; using Microsoft.CodeAnalysis.Razor;
using Microsoft.CodeAnalysis.Razor.ProjectSystem; using Microsoft.CodeAnalysis.Razor.ProjectSystem;
@ -12,18 +11,6 @@ namespace Microsoft.VisualStudio.Editor.Razor
{ {
internal class DefaultTagHelperResolver : TagHelperResolver 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) public override Task<TagHelperResolutionResult> GetTagHelpersAsync(ProjectSnapshot project, CancellationToken cancellationToken = default)
{ {
if (project == null) if (project == null)
@ -36,8 +23,7 @@ namespace Microsoft.VisualStudio.Editor.Razor
return Task.FromResult(TagHelperResolutionResult.Empty); return Task.FromResult(TagHelperResolutionResult.Empty);
} }
var engine = _engineFactory.Create(project, RazorProjectFileSystem.Empty, b => { }); return GetTagHelpersAsync(project, project.GetProjectEngine());
return GetTagHelpersAsync(project, engine);
} }
} }
} }

View File

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

View File

@ -3,6 +3,9 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.Language;
using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Razor; using Microsoft.CodeAnalysis.Razor;
@ -25,7 +28,13 @@ namespace Microsoft.VisualStudio.Editor.Razor
private readonly List<ITextView> _textViews; private readonly List<ITextView> _textViews;
private readonly Workspace _workspace; private readonly Workspace _workspace;
private bool _isSupportedProject; 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; 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. _workspace = workspace; // For now we assume that the workspace is the always default VS workspace.
_textViews = new List<ITextView>(); _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 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 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; public override ITextBuffer TextBuffer => _textBuffer;
@ -111,6 +126,8 @@ namespace Microsoft.VisualStudio.Editor.Razor
public override string ProjectPath => _projectPath; public override string ProjectPath => _projectPath;
public Task PendingTagHelperTask => _computingTagHelpers.task ?? Task.CompletedTask;
internal void AddTextView(ITextView textView) internal void AddTextView(ITextView textView)
{ {
if (textView == null) if (textView == null)
@ -118,6 +135,8 @@ namespace Microsoft.VisualStudio.Editor.Razor
throw new ArgumentNullException(nameof(textView)); throw new ArgumentNullException(nameof(textView));
} }
_foregroundDispatcher.AssertForegroundThread();
if (!_textViews.Contains(textView)) if (!_textViews.Contains(textView))
{ {
_textViews.Add(textView); _textViews.Add(textView);
@ -131,6 +150,8 @@ namespace Microsoft.VisualStudio.Editor.Razor
throw new ArgumentNullException(nameof(textView)); throw new ArgumentNullException(nameof(textView));
} }
_foregroundDispatcher.AssertForegroundThread();
if (_textViews.Contains(textView)) if (_textViews.Contains(textView))
{ {
_textViews.Remove(textView); _textViews.Remove(textView);
@ -139,6 +160,8 @@ namespace Microsoft.VisualStudio.Editor.Razor
public override ITextView GetFocusedTextView() public override ITextView GetFocusedTextView()
{ {
_foregroundDispatcher.AssertForegroundThread();
for (var i = 0; i < TextViews.Count; i++) for (var i = 0; i < TextViews.Count; i++)
{ {
if (TextViews[i].HasAggregateFocus) if (TextViews[i].HasAggregateFocus)
@ -152,20 +175,23 @@ namespace Microsoft.VisualStudio.Editor.Razor
public void Subscribe() 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); _importDocumentManager.OnSubscribed(this);
_workspaceEditorSettings.Changed += EditorSettingsManager_Changed; OnContextChanged(ContextChangeKind.ProjectChanged);
_projectManager.Changed += ProjectManager_Changed;
_importDocumentManager.Changed += Import_Changed;
_isSupportedProject = true;
_project = _projectManager.GetProjectWithFilePath(_projectPath);
OnContextChanged(_project, ContextChangeKind.ProjectChanged);
} }
public void Unsubscribe() public void Unsubscribe()
{ {
_foregroundDispatcher.AssertForegroundThread();
_importDocumentManager.OnUnsubscribed(this); _importDocumentManager.OnUnsubscribed(this);
_projectManager.Changed -= ProjectManager_Changed; _projectManager.Changed -= ProjectManager_Changed;
@ -174,37 +200,112 @@ namespace Microsoft.VisualStudio.Editor.Razor
// Detached from project. // Detached from project.
_isSupportedProject = false; _isSupportedProject = false;
_project = null; _projectSnapshot = null;
OnContextChanged(kind: ContextChangeKind.ProjectChanged);
OnContextChanged(project: null, kind: ContextChangeKind.ProjectChanged);
} }
private void OnContextChanged(ProjectSnapshot project, ContextChangeKind kind) private void StartComputingTagHelpers()
{ {
_foregroundDispatcher.AssertForegroundThread(); _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; var handler = ContextChanged;
if (handler != null) if (handler != null)
{ {
handler(this, new ContextChangeEventArgs(kind)); handler(this, new ContextChangeEventArgs(kind));
} }
if (kind == ContextChangeKind.ProjectChanged &&
_projectSnapshot != null &&
_computingTagHelpers.project == null)
{
StartComputingTagHelpers();
}
} }
// Internal for testing // Internal for testing
internal void ProjectManager_Changed(object sender, ProjectChangeEventArgs e) internal void ProjectManager_Changed(object sender, ProjectChangeEventArgs e)
{ {
_foregroundDispatcher.AssertForegroundThread();
if (_projectPath != null && 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); case ProjectChangeKind.DocumentsChanged:
}
else // Nothing to do.
{ break;
OnContextChanged(e.Project, ContextChangeKind.ProjectChanged);
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 for testing
internal void EditorSettingsManager_Changed(object sender, EditorSettingsChangedEventArgs args) internal void EditorSettingsManager_Changed(object sender, EditorSettingsChangedEventArgs args)
{ {
OnContextChanged(_project, ContextChangeKind.EditorSettingsChanged); _foregroundDispatcher.AssertForegroundThread();
OnContextChanged(ContextChangeKind.EditorSettingsChanged);
} }
// Internal for testing // Internal for testing
internal void Import_Changed(object sender, ImportChangedEventArgs args) internal void Import_Changed(object sender, ImportChangedEventArgs args)
{ {
_foregroundDispatcher.AssertForegroundThread();
foreach (var path in args.AssociatedDocuments) foreach (var path in args.AssociatedDocuments)
{ {
if (string.Equals(_filePath, path, StringComparison.OrdinalIgnoreCase)) if (string.Equals(_filePath, path, StringComparison.OrdinalIgnoreCase))
{ {
OnContextChanged(_project, ContextChangeKind.ImportsChanged); OnContextChanged(ContextChangeKind.ImportsChanged);
break; break;
} }
} }

View File

@ -3,6 +3,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.IO; using System.IO;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -31,7 +32,7 @@ namespace Microsoft.VisualStudio.Editor.Razor
private readonly VisualStudioCompletionBroker _completionBroker; private readonly VisualStudioCompletionBroker _completionBroker;
private readonly VisualStudioDocumentTracker _documentTracker; private readonly VisualStudioDocumentTracker _documentTracker;
private readonly ForegroundDispatcher _dispatcher; private readonly ForegroundDispatcher _dispatcher;
private readonly RazorProjectEngineFactoryService _projectEngineFactory; private readonly ProjectSnapshotProjectEngineFactory _projectEngineFactory;
private readonly ErrorReporter _errorReporter; private readonly ErrorReporter _errorReporter;
private RazorProjectEngine _projectEngine; private RazorProjectEngine _projectEngine;
private RazorCodeDocument _codeDocument; private RazorCodeDocument _codeDocument;
@ -47,7 +48,7 @@ namespace Microsoft.VisualStudio.Editor.Razor
public DefaultVisualStudioRazorParser( public DefaultVisualStudioRazorParser(
ForegroundDispatcher dispatcher, ForegroundDispatcher dispatcher,
VisualStudioDocumentTracker documentTracker, VisualStudioDocumentTracker documentTracker,
RazorProjectEngineFactoryService projectEngineFactory, ProjectSnapshotProjectEngineFactory projectEngineFactory,
ErrorReporter errorReporter, ErrorReporter errorReporter,
VisualStudioCompletionBroker completionBroker) VisualStudioCompletionBroker completionBroker)
{ {
@ -167,8 +168,18 @@ namespace Microsoft.VisualStudio.Editor.Razor
{ {
_dispatcher.AssertForegroundThread(); _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); var projectDirectory = Path.GetDirectoryName(_documentTracker.ProjectPath);
_projectEngine = _projectEngineFactory.Create(projectDirectory, ConfigureProjectEngine);
_parser = new BackgroundParser(_projectEngine, FilePath, projectDirectory); _parser = new BackgroundParser(_projectEngine, FilePath, projectDirectory);
_parser.ResultsReady += OnResultsReady; _parser.ResultsReady += OnResultsReady;
_parser.Start(); _parser.Start();

View File

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

View File

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

View File

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

@ -59,6 +59,9 @@
<None Include="$(RulesDirectory)RazorGeneral.xaml"> <None Include="$(RulesDirectory)RazorGeneral.xaml">
<Link>ProjectSystem\Rules\RazorGeneral.xaml</Link> <Link>ProjectSystem\Rules\RazorGeneral.xaml</Link>
</None> </None>
<None Include="$(RulesDirectory)RazorGenerateWithTargetPath.xaml">
<Link>ProjectSystem\Rules\RazorGenerateWithTargetPath.xaml</Link>
</None>
<EmbeddedResource Include="$(RulesDirectory)RazorConfiguration.xaml"> <EmbeddedResource Include="$(RulesDirectory)RazorConfiguration.xaml">
<LogicalName>XamlRuleToCode:RazorConfiguration.xaml</LogicalName> <LogicalName>XamlRuleToCode:RazorConfiguration.xaml</LogicalName>
</EmbeddedResource> </EmbeddedResource>
@ -68,6 +71,9 @@
<EmbeddedResource Include="$(RulesDirectory)RazorGeneral.xaml"> <EmbeddedResource Include="$(RulesDirectory)RazorGeneral.xaml">
<LogicalName>XamlRuleToCode:RazorGeneral.xaml</LogicalName> <LogicalName>XamlRuleToCode:RazorGeneral.xaml</LogicalName>
</EmbeddedResource> </EmbeddedResource>
<EmbeddedResource Include="$(RulesDirectory)RazorGenerateWithTargetPath.xaml">
<LogicalName>XamlRuleToCode:RazorGenerateWithTargetPath.xaml</LogicalName>
</EmbeddedResource>
</ItemGroup> </ItemGroup>
<ItemGroup Condition="'$(MSBuildRuntimeType)'!='Core'"> <ItemGroup Condition="'$(MSBuildRuntimeType)'!='Core'">
<XamlPropertyRule Include="$(RulesDirectory)RazorConfiguration.xaml"> <XamlPropertyRule Include="$(RulesDirectory)RazorConfiguration.xaml">
@ -92,6 +98,13 @@
<RuleInjectionClassName>RazorProjectProperties</RuleInjectionClassName> <RuleInjectionClassName>RazorProjectProperties</RuleInjectionClassName>
<OutputPath>ProjectSystem\Rules\</OutputPath> <OutputPath>ProjectSystem\Rules\</OutputPath>
</XamlPropertyRule> </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>
<ItemGroup> <ItemGroup>
<Compile Update="ProjectSystem\Rules\RazorConfiguration.cs"> <Compile Update="ProjectSystem\Rules\RazorConfiguration.cs">
@ -103,6 +116,9 @@
<Compile Update="ProjectSystem\Rules\RazorGeneral.cs"> <Compile Update="ProjectSystem\Rules\RazorGeneral.cs">
<DependentUpon>ProjectSystem\Rules\RazorGeneral.xaml</DependentUpon> <DependentUpon>ProjectSystem\Rules\RazorGeneral.xaml</DependentUpon>
</Compile> </Compile>
<Compile Update="ProjectSystem\Rules\RazorGenerateWithTargetPath.cs">
<DependentUpon>ProjectSystem\Rules\RazorGenerateWithTargetPath.xaml</DependentUpon>
</Compile>
</ItemGroup> </ItemGroup>
<!-- <!--

View File

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

View File

@ -1,21 +1,19 @@
// Copyright (c) .NET Foundation. All rights reserved. // Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Composition;
using Microsoft.CodeAnalysis.Host; using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.Razor; using Microsoft.CodeAnalysis.Razor;
namespace Microsoft.VisualStudio.LanguageServices.Razor namespace Microsoft.VisualStudio.LanguageServices.Razor
{ {
[Shared]
[ExportLanguageServiceFactory(typeof(TagHelperResolver), RazorLanguage.Name, ServiceLayer.Host)] [ExportLanguageServiceFactory(typeof(TagHelperResolver), RazorLanguage.Name, ServiceLayer.Host)]
internal class OOPTagHelperResolverFactory : ILanguageServiceFactory internal class OOPTagHelperResolverFactory : ILanguageServiceFactory
{ {
public ILanguageService CreateLanguageService(HostLanguageServices languageServices) public ILanguageService CreateLanguageService(HostLanguageServices languageServices)
{ {
return new OOPTagHelperResolver( return new OOPTagHelperResolver(
languageServices.GetRequiredService<RazorProjectEngineFactoryService>(), languageServices.WorkspaceServices.GetRequiredService<ProjectSnapshotProjectEngineFactory>(),
languageServices.WorkspaceServices.GetRequiredService<ErrorReporter>(), languageServices.WorkspaceServices.GetRequiredService<ErrorReporter>(),
languageServices.WorkspaceServices.Workspace); languageServices.WorkspaceServices.Workspace);
} }

View File

@ -5,6 +5,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Immutable; using System.Collections.Immutable;
using System.ComponentModel.Composition; using System.ComponentModel.Composition;
using System.IO;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -13,8 +14,7 @@ using Microsoft.AspNetCore.Razor.Language;
using Microsoft.VisualStudio.LanguageServices; using Microsoft.VisualStudio.LanguageServices;
using Microsoft.VisualStudio.ProjectSystem; using Microsoft.VisualStudio.ProjectSystem;
using Microsoft.VisualStudio.ProjectSystem.Properties; using Microsoft.VisualStudio.ProjectSystem.Properties;
using ProjectState = System.Collections.Immutable.IImmutableDictionary<string, Microsoft.VisualStudio.ProjectSystem.Properties.IProjectRuleSnapshot>; using Item = System.Collections.Generic.KeyValuePair<string, System.Collections.Immutable.IImmutableDictionary<string, string>>;
using ProjectStateItem = System.Collections.Generic.KeyValuePair<string, System.Collections.Immutable.IImmutableDictionary<string, string>>;
namespace Microsoft.CodeAnalysis.Razor.ProjectSystem namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
{ {
@ -62,7 +62,13 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
receiver, receiver,
initialDataAsNew: true, initialDataAsNew: true,
suppressVersionOnlyUpdates: 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) protected override async Task DisposeCoreAsync(bool initialized)
@ -90,12 +96,36 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
if (TryGetConfiguration(update.Value.CurrentState, out var configuration)) if (TryGetConfiguration(update.Value.CurrentState, out var configuration))
{ {
var hostProject = new HostProject(CommonServices.UnconfiguredProject.FullPath, 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 else
{ {
// Ok we can't find a configuration. Let's assume this project isn't using Razor then. // 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); }, registerFaultHandler: true);
@ -103,34 +133,34 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
// Internal for testing // Internal for testing
internal static bool TryGetConfiguration( internal static bool TryGetConfiguration(
ProjectState projectState, IImmutableDictionary<string, IProjectRuleSnapshot> state,
out RazorConfiguration configuration) out RazorConfiguration configuration)
{ {
if (!TryGetDefaultConfiguration(projectState, out var defaultConfiguration)) if (!TryGetDefaultConfiguration(state, out var defaultConfiguration))
{ {
configuration = null; configuration = null;
return false; return false;
} }
if (!TryGetLanguageVersion(projectState, out var languageVersion)) if (!TryGetLanguageVersion(state, out var languageVersion))
{ {
configuration = null; configuration = null;
return false; return false;
} }
if (!TryGetConfigurationItem(defaultConfiguration, projectState, out var configurationItem)) if (!TryGetConfigurationItem(defaultConfiguration, state, out var configurationItem))
{ {
configuration = null; configuration = null;
return false; return false;
} }
if (!TryGetConfiguredExtensionNames(configurationItem, out var configuredExtensionNames)) if (!TryGetExtensionNames(configurationItem, out var extensionNames))
{ {
configuration = null; configuration = null;
return false; return false;
} }
if (!TryGetExtensions(configuredExtensionNames, projectState, out var extensions)) if (!TryGetExtensions(extensionNames, state, out var extensions))
{ {
configuration = null; configuration = null;
return false; return false;
@ -142,9 +172,11 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
// Internal for testing // 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; defaultConfiguration = null;
return false; return false;
@ -166,9 +198,11 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
} }
// Internal for testing // 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; languageVersion = null;
return false; return false;
@ -197,17 +231,17 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
// Internal for testing // Internal for testing
internal static bool TryGetConfigurationItem( internal static bool TryGetConfigurationItem(
string configuration, string configuration,
ProjectState projectState, IImmutableDictionary<string, IProjectRuleSnapshot> state,
out ProjectStateItem configurationItem) 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; return false;
} }
var razorConfigurationItems = configurationState.Items; var items = configurationState.Items;
foreach (var item in razorConfigurationItems) foreach (var item in items)
{ {
if (item.Key == configuration) if (item.Key == configuration)
{ {
@ -216,44 +250,49 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
} }
} }
configurationItem = default(ProjectStateItem); configurationItem = default(Item);
return false; return false;
} }
// Internal for testing // 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; configuredExtensionNames = null;
return false; return false;
} }
if (string.IsNullOrEmpty(extensionNamesValue)) if (string.IsNullOrEmpty(extensionNames))
{ {
configuredExtensionNames = null; configuredExtensionNames = null;
return false; return false;
} }
configuredExtensionNames = extensionNamesValue.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries); configuredExtensionNames = extensionNames.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries);
return true; return true;
} }
// Internal for testing // 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; extensions = null;
return false; return false;
} }
var extensionItems = extensionState.Items; var items = rule.Items;
var extensionList = new List<ProjectSystemRazorExtension>(); var extensionList = new List<ProjectSystemRazorExtension>();
foreach (var item in extensionItems) foreach (var item in items)
{ {
var extensionName = item.Key; var extensionName = item.Key;
if (configuredExtensionNames.Contains(extensionName)) if (extensionNames.Contains(extensionName))
{ {
extensionList.Add(new ProjectSystemRazorExtension(extensionName)); extensionList.Add(new ProjectSystemRazorExtension(extensionName));
} }
@ -262,5 +301,53 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
extensions = extensionList.ToArray(); extensions = extensionList.ToArray();
return true; 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,6 +85,8 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
await ExecuteWithLock(async () => await ExecuteWithLock(async () =>
{ {
string mvcReferenceFullPath = null; string mvcReferenceFullPath = null;
if (update.Value.CurrentState.ContainsKey(ResolvedCompilationReference.SchemaName))
{
var references = update.Value.CurrentState[ResolvedCompilationReference.SchemaName].Items; var references = update.Value.CurrentState[ResolvedCompilationReference.SchemaName].Items;
foreach (var reference in references) foreach (var reference in references)
{ {
@ -94,11 +96,12 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
break; break;
} }
} }
}
if (mvcReferenceFullPath == null) if (mvcReferenceFullPath == null)
{ {
// Ok we can't find an MVC version. Let's assume this project isn't using Razor then. // 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; return;
} }
@ -106,13 +109,16 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
if (version == null) if (version == null)
{ {
// Ok we can't find an MVC version. Let's assume this project isn't using Razor then. // 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; return;
} }
var configuration = FallbackRazorConfiguration.SelectConfiguration(version); var configuration = FallbackRazorConfiguration.SelectConfiguration(version);
var hostProject = new HostProject(CommonServices.UnconfiguredProject.FullPath, configuration); var hostProject = new HostProject(CommonServices.UnconfiguredProject.FullPath, configuration);
await UpdateProjectUnsafeAsync(hostProject).ConfigureAwait(false); await UpdateAsync(() =>
{
UpdateProjectUnsafe(hostProject);
}).ConfigureAwait(false);
}); });
}, registerFaultHandler: true); }, 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.Collections.Generic;
using System.ComponentModel.Composition; using System.ComponentModel.Composition;
using System.Diagnostics;
using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.VisualStudio.LanguageServices; using Microsoft.VisualStudio.LanguageServices;
@ -18,6 +21,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
private ProjectSnapshotManagerBase _projectManager; private ProjectSnapshotManagerBase _projectManager;
private HostProject _current; private HostProject _current;
private Dictionary<string, HostDocument> _currentDocuments;
public RazorProjectHostBase( public RazorProjectHostBase(
IUnconfiguredProjectCommonServices commonServices, IUnconfiguredProjectCommonServices commonServices,
@ -38,6 +42,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
_workspace = workspace; _workspace = workspace;
_lock = new AsyncSemaphore(initialCount: 1); _lock = new AsyncSemaphore(initialCount: 1);
_currentDocuments = new Dictionary<string, HostDocument>(FilePathComparer.Instance);
} }
// Internal for testing // Internal for testing
@ -67,8 +72,11 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
_projectManager = projectManager; _projectManager = projectManager;
_lock = new AsyncSemaphore(initialCount: 1); _lock = new AsyncSemaphore(initialCount: 1);
_currentDocuments = new Dictionary<string, HostDocument>(FilePathComparer.Instance);
} }
protected HostProject Current => _current;
protected IUnconfiguredProjectCommonServices CommonServices { get; } protected IUnconfiguredProjectCommonServices CommonServices { get; }
// internal for tests. The product will call through the IProjectDynamicLoadComponent interface. // internal for tests. The product will call through the IProjectDynamicLoadComponent interface.
@ -94,7 +102,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
{ {
if (_current != null) 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) if (_current != null)
{ {
var old = _current; var old = _current;
await UpdateProjectUnsafeAsync(null).ConfigureAwait(false); var oldDocuments = _currentDocuments.Values.ToArray();
await UpdateAsync(UninitializeProjectUnsafe).ConfigureAwait(false);
await UpdateAsync(() =>
{
var filePath = CommonServices.UnconfiguredProject.FullPath; var filePath = CommonServices.UnconfiguredProject.FullPath;
await UpdateProjectUnsafeAsync(new HostProject(filePath, old.Configuration)).ConfigureAwait(false); 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; return _projectManager;
} }
// Must be called inside the lock. protected async Task UpdateAsync(Action action)
protected async Task UpdateProjectUnsafeAsync(HostProject project)
{ {
await CommonServices.ThreadingService.SwitchToUIThread(); 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) if (_current == null && project == null)
{ {
// This is a no-op. This project isn't using Razor. // 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) else if (_current != null && project == null)
{ {
Debug.Assert(_currentDocuments.Count == 0);
projectManager.HostProjectRemoved(_current); projectManager.HostProjectRemoved(_current);
} }
else else
@ -160,6 +189,40 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
_current = project; _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) protected async Task ExecuteWithLock(Func<Task> func)
{ {
using (JoinableCollection.Join()) using (JoinableCollection.Join())

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

View File

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

View File

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

View File

@ -13,14 +13,14 @@ namespace Microsoft.CodeAnalysis.Host
private static readonly Workspace DefaultWorkspace = TestWorkspace.Create(); private static readonly Workspace DefaultWorkspace = TestWorkspace.Create();
private readonly HostServices _hostServices; private readonly HostServices _hostServices;
private readonly HostLanguageServices _razorLanguageServices;
private readonly IEnumerable<IWorkspaceService> _workspaceServices; private readonly IEnumerable<IWorkspaceService> _workspaceServices;
private readonly TestRazorLanguageServices _razorLanguageServices;
private readonly Workspace _workspace; private readonly Workspace _workspace;
public TestWorkspaceServices( public TestWorkspaceServices(
HostServices hostServices, HostServices hostServices,
IEnumerable<IWorkspaceService> workspaceServices, IEnumerable<IWorkspaceService> workspaceServices,
IEnumerable<ILanguageService> razorLanguageServices, IEnumerable<ILanguageService> languageServices,
Workspace workspace) Workspace workspace)
{ {
if (hostServices == null) if (hostServices == null)
@ -33,9 +33,9 @@ namespace Microsoft.CodeAnalysis.Host
throw new ArgumentNullException(nameof(workspaceServices)); throw new ArgumentNullException(nameof(workspaceServices));
} }
if (razorLanguageServices == null) if (languageServices == null)
{ {
throw new ArgumentNullException(nameof(razorLanguageServices)); throw new ArgumentNullException(nameof(languageServices));
} }
if (workspace == null) if (workspace == null)
@ -45,8 +45,9 @@ namespace Microsoft.CodeAnalysis.Host
_hostServices = hostServices; _hostServices = hostServices;
_workspaceServices = workspaceServices; _workspaceServices = workspaceServices;
_razorLanguageServices = new TestRazorLanguageServices(this, razorLanguageServices);
_workspace = workspace; _workspace = workspace;
_razorLanguageServices = new TestLanguageServices(this, languageServices);
} }
public override HostServices HostServices => _hostServices; public override HostServices HostServices => _hostServices;
@ -68,12 +69,13 @@ namespace Microsoft.CodeAnalysis.Host
public override HostLanguageServices GetLanguageServices(string languageName) 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 }; 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.Language;
using Microsoft.CodeAnalysis.Host;
using Xunit; using Xunit;
namespace Microsoft.CodeAnalysis.Razor.ProjectSystem namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
{ {
public class DefaultProjectSnapshotTest public class DefaultProjectSnapshotTest
{ {
[Fact] public DefaultProjectSnapshotTest()
public void WithWorkspaceProject_CreatesSnapshot_UpdatesUnderlyingProject()
{ {
// Arrange TagHelperResolver = new TestTagHelperResolver();
var hostProject = new HostProject("Test.cshtml", FallbackRazorConfiguration.MVC_2_0);
var workspaceProject = GetWorkspaceProject("Test1");
var original = new DefaultProjectSnapshot(hostProject, workspaceProject);
var anotherProject = GetWorkspaceProject("Test1"); HostServices = TestServices.Create(
new IWorkspaceService[]
// 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 new TestProjectSnapshotProjectEngineFactory(),
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[]
{
TagHelperDescriptorBuilder.Create("One", "TestAssembly").Build(),
TagHelperDescriptorBuilder.Create("Two", "TestAssembly").Build(),
}, },
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; // Arrange
TestWorkspace.Create(workspace => 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()
{ {
project = workspace.AddProject(name, LanguageNames.CSharp); // Arrange
}); TagHelperResolver.CompletionSource = new TaskCompletionSource<TagHelperResolutionResult>();
return project;
try
{
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.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Moq; using Moq;
using Xunit; using Xunit;
@ -214,23 +212,9 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
private class TestProjectSnapshotManager : DefaultProjectSnapshotManager private class TestProjectSnapshotManager : DefaultProjectSnapshotManager
{ {
public TestProjectSnapshotManager(IEnumerable<ProjectSnapshotChangeTrigger> triggers, Workspace workspace) public TestProjectSnapshotManager(IEnumerable<ProjectSnapshotChangeTrigger> triggers, Workspace workspace)
: base(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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.IO;
using Microsoft.AspNetCore.Mvc.Razor.Extensions; using Microsoft.AspNetCore.Mvc.Razor.Extensions;
using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.Language;
using Microsoft.CodeAnalysis.Razor; using Microsoft.CodeAnalysis.Razor;
@ -13,23 +14,51 @@ namespace Microsoft.VisualStudio.Editor.Razor
{ {
public class DefaultImportDocumentManagerIntegrationTest : ForegroundDispatcherTestBase 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] [ForegroundFact]
public void Changed_TrackerChanged_ResultsInChangedHavingCorrectArgs() public void Changed_TrackerChanged_ResultsInChangedHavingCorrectArgs()
{ {
// Arrange // 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 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 tracker = Mock.Of<VisualStudioDocumentTracker>(
var projectEngineFactoryService = GetProjectEngineFactoryService(); t => t.FilePath == "C:\\path\\to\\project\\Views\\Home\\file.cshtml" &&
var fileChangeTracker = new Mock<FileChangeTracker>(); t.ProjectPath == ProjectPath &&
fileChangeTracker.Setup(f => f.FilePath).Returns(testImportsPath); 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 fileChangeTrackerFactory = new Mock<FileChangeTrackerFactory>();
var fileChangeTracker = new Mock<FileChangeTracker>();
fileChangeTracker
.Setup(f => f.FilePath)
.Returns(testImportsPath);
fileChangeTrackerFactory fileChangeTrackerFactory
.Setup(f => f.Create(testImportsPath)) .Setup(f => f.Create(testImportsPath))
.Returns(fileChangeTracker.Object); .Returns(fileChangeTracker.Object);
fileChangeTrackerFactory fileChangeTrackerFactory
.Setup(f => f.Create("C:\\path\\to\\project\\Views\\_ViewImports.cshtml")) .Setup(f => f.Create("C:\\path\\to\\project\\Views\\_ViewImports.cshtml"))
.Returns(Mock.Of<FileChangeTracker>()); .Returns(Mock.Of<FileChangeTracker>());
@ -38,7 +67,7 @@ namespace Microsoft.VisualStudio.Editor.Razor
.Returns(Mock.Of<FileChangeTracker>()); .Returns(Mock.Of<FileChangeTracker>());
var called = false; 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(tracker);
manager.OnSubscribed(anotherTracker); manager.OnSubscribed(anotherTracker);
manager.Changed += (sender, args) => manager.Changed += (sender, args) =>
@ -49,8 +78,8 @@ namespace Microsoft.VisualStudio.Editor.Razor
Assert.Equal(FileChangeKind.Changed, args.Kind); Assert.Equal(FileChangeKind.Changed, args.Kind);
Assert.Collection( Assert.Collection(
args.AssociatedDocuments, args.AssociatedDocuments,
f => Assert.Equal(filePath, f), f => Assert.Equal("C:\\path\\to\\project\\Views\\Home\\file.cshtml", f),
f => Assert.Equal(anotherFilePath, f)); f => Assert.Equal("C:\\path\\to\\project\\anotherFile.cshtml", f));
}; };
// Act // Act
@ -59,25 +88,5 @@ namespace Microsoft.VisualStudio.Editor.Razor
// Assert // Assert
Assert.True(called); 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using Microsoft.AspNetCore.Mvc.Razor.Extensions; using System.IO;
using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.Language;
using Microsoft.CodeAnalysis.Razor; using Microsoft.CodeAnalysis.Razor;
using Microsoft.CodeAnalysis.Razor.ProjectSystem; using Microsoft.CodeAnalysis.Razor.ProjectSystem;
@ -13,23 +13,48 @@ namespace Microsoft.VisualStudio.Editor.Razor
{ {
public class DefaultImportDocumentManagerTest : ForegroundDispatcherTestBase 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] [ForegroundFact]
public void OnSubscribed_StartsFileChangeTrackers() public void OnSubscribed_StartsFileChangeTrackers()
{ {
// Arrange // Arrange
var filePath = "C:\\path\\to\\project\\Views\\Home\\file.cshtml"; var tracker = Mock.Of<VisualStudioDocumentTracker>(
var projectPath = "C:\\path\\to\\project\\project.csproj"; t => t.FilePath == "C:\\path\\to\\project\\Views\\Home\\file.cshtml" &&
var tracker = Mock.Of<VisualStudioDocumentTracker>(t => t.FilePath == filePath && t.ProjectPath == projectPath); t.ProjectPath == ProjectPath &&
var projectEngineService = GetProjectEngineFactoryService(); t.ProjectSnapshot == Mock.Of<ProjectSnapshot>(p => p.GetProjectEngine() == ProjectEngine));
var fileChangeTracker1 = new Mock<FileChangeTracker>();
fileChangeTracker1.Setup(f => f.StartListening()).Verifiable();
var fileChangeTrackerFactory = new Mock<FileChangeTrackerFactory>(); var fileChangeTrackerFactory = new Mock<FileChangeTrackerFactory>();
var fileChangeTracker1 = new Mock<FileChangeTracker>();
fileChangeTracker1
.Setup(f => f.StartListening())
.Verifiable();
fileChangeTrackerFactory fileChangeTrackerFactory
.Setup(f => f.Create("C:\\path\\to\\project\\Views\\Home\\_ViewImports.cshtml")) .Setup(f => f.Create("C:\\path\\to\\project\\Views\\Home\\_ViewImports.cshtml"))
.Returns(fileChangeTracker1.Object) .Returns(fileChangeTracker1.Object)
.Verifiable(); .Verifiable();
var fileChangeTracker2 = new Mock<FileChangeTracker>(); var fileChangeTracker2 = new Mock<FileChangeTracker>();
fileChangeTracker2.Setup(f => f.StartListening()).Verifiable(); fileChangeTracker2
.Setup(f => f.StartListening())
.Verifiable();
fileChangeTrackerFactory fileChangeTrackerFactory
.Setup(f => f.Create("C:\\path\\to\\project\\Views\\_ViewImports.cshtml")) .Setup(f => f.Create("C:\\path\\to\\project\\Views\\_ViewImports.cshtml"))
.Returns(fileChangeTracker2.Object) .Returns(fileChangeTracker2.Object)
@ -41,7 +66,7 @@ namespace Microsoft.VisualStudio.Editor.Razor
.Returns(fileChangeTracker3.Object) .Returns(fileChangeTracker3.Object)
.Verifiable(); .Verifiable();
var manager = new DefaultImportDocumentManager(Dispatcher, new DefaultErrorReporter(), fileChangeTrackerFactory.Object, projectEngineService); var manager = new DefaultImportDocumentManager(Dispatcher, new DefaultErrorReporter(), fileChangeTrackerFactory.Object);
// Act // Act
manager.OnSubscribed(tracker); manager.OnSubscribed(tracker);
@ -57,10 +82,15 @@ namespace Microsoft.VisualStudio.Editor.Razor
public void OnSubscribed_AlreadySubscribed_DoesNothing() public void OnSubscribed_AlreadySubscribed_DoesNothing()
{ {
// Arrange // Arrange
var filePath = "C:\\path\\to\\project\\file.cshtml"; var tracker = Mock.Of<VisualStudioDocumentTracker>(
var projectPath = "C:\\path\\to\\project\\project.csproj"; t => t.FilePath == "C:\\path\\to\\project\\file.cshtml" &&
var tracker = Mock.Of<VisualStudioDocumentTracker>(t => t.FilePath == filePath && t.ProjectPath == projectPath); t.ProjectPath == ProjectPath &&
var projectEngineService = GetProjectEngineFactoryService(); 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 callCount = 0;
var fileChangeTrackerFactory = new Mock<FileChangeTrackerFactory>(); var fileChangeTrackerFactory = new Mock<FileChangeTrackerFactory>();
@ -69,12 +99,9 @@ namespace Microsoft.VisualStudio.Editor.Razor
.Returns(Mock.Of<FileChangeTracker>()) .Returns(Mock.Of<FileChangeTracker>())
.Callback(() => callCount++); .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. 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 // Act
manager.OnSubscribed(anotherTracker); manager.OnSubscribed(anotherTracker);
@ -86,20 +113,22 @@ namespace Microsoft.VisualStudio.Editor.Razor
public void OnUnsubscribed_StopsFileChangeTracker() public void OnUnsubscribed_StopsFileChangeTracker()
{ {
// Arrange // Arrange
var filePath = "C:\\path\\to\\project\\file.cshtml"; var tracker = Mock.Of<VisualStudioDocumentTracker>(
var projectPath = "C:\\path\\to\\project\\project.csproj"; t => t.FilePath == "C:\\path\\to\\project\\file.cshtml" &&
var tracker = Mock.Of<VisualStudioDocumentTracker>(t => t.FilePath == filePath && t.ProjectPath == projectPath); t.ProjectPath == ProjectPath &&
var projectEngineService = GetProjectEngineFactoryService(); 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 fileChangeTrackerFactory = new Mock<FileChangeTrackerFactory>(MockBehavior.Strict);
var fileChangeTracker = new Mock<FileChangeTracker>();
fileChangeTracker
.Setup(f => f.StopListening())
.Verifiable();
fileChangeTrackerFactory fileChangeTrackerFactory
.Setup(f => f.Create("C:\\path\\to\\project\\_ViewImports.cshtml")) .Setup(f => f.Create("C:\\path\\to\\project\\_ViewImports.cshtml"))
.Returns(fileChangeTracker.Object) .Returns(fileChangeTracker.Object)
.Verifiable(); .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. manager.OnSubscribed(tracker); // Start tracking the import.
// Act // Act
@ -114,49 +143,33 @@ namespace Microsoft.VisualStudio.Editor.Razor
public void OnUnsubscribed_AnotherDocumentTrackingImport_DoesNotStopFileChangeTracker() public void OnUnsubscribed_AnotherDocumentTrackingImport_DoesNotStopFileChangeTracker()
{ {
// Arrange // Arrange
var filePath = "C:\\path\\to\\project\\file.cshtml"; var tracker = Mock.Of<VisualStudioDocumentTracker>(
var projectPath = "C:\\path\\to\\project\\project.csproj"; t => t.FilePath == "C:\\path\\to\\project\\file.cshtml" &&
var tracker = Mock.Of<VisualStudioDocumentTracker>(t => t.FilePath == filePath && t.ProjectPath == projectPath); t.ProjectPath == ProjectPath &&
var projectEngineService = GetProjectEngineFactoryService(); 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>(); var fileChangeTracker = new Mock<FileChangeTracker>();
fileChangeTracker fileChangeTracker
.Setup(f => f.StopListening()) .Setup(f => f.StopListening())
.Throws(new InvalidOperationException()); .Throws(new InvalidOperationException());
var fileChangeTrackerFactory = new Mock<FileChangeTrackerFactory>();
fileChangeTrackerFactory fileChangeTrackerFactory
.Setup(f => f.Create(It.IsAny<string>())) .Setup(f => f.Create(It.IsAny<string>()))
.Returns(fileChangeTracker.Object); .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. 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. manager.OnSubscribed(anotherTracker); // Starts tracking import for the second document.
// Act & Assert (Does not throw) // Act & Assert (Does not throw)
manager.OnUnsubscribed(tracker); manager.OnUnsubscribed(tracker);
} 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;
} }
} }
} }

View File

@ -5,19 +5,19 @@ using System;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.Language;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Razor;
using Microsoft.CodeAnalysis.Razor.ProjectSystem; using Microsoft.CodeAnalysis.Razor.ProjectSystem;
using Moq; using Microsoft.VisualStudio.Editor.Razor;
using Xunit; using Xunit;
using Mvc1_X = Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X; using Mvc1_X = Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X;
using MvcLatest = Microsoft.AspNetCore.Mvc.Razor.Extensions; 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; Project project = null;
@ -40,6 +40,11 @@ namespace Microsoft.VisualStudio.Editor.Razor
"/TestPath/SomePath/Test.csproj", "/TestPath/SomePath/Test.csproj",
new ProjectSystemRazorConfiguration(RazorLanguageVersion.Version_2_1, "Blazor-0.1", Array.Empty<RazorExtension>())); 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>[] CustomFactories = new Lazy<IProjectEngineFactory, ICustomProjectEngineFactoryMetadata>[]
{ {
@ -74,7 +79,16 @@ namespace Microsoft.VisualStudio.Editor.Razor
private HostProject HostProject_For_UnknownConfiguration { get; } 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 Project WorkspaceProject { get; }
private Workspace Workspace { get; } private Workspace Workspace { get; }
@ -83,14 +97,12 @@ namespace Microsoft.VisualStudio.Editor.Razor
public void Create_CreatesDesignTimeTemplateEngine_ForVersion2_1() public void Create_CreatesDesignTimeTemplateEngine_ForVersion2_1()
{ {
// Arrange // Arrange
var projectManager = new TestProjectSnapshotManager(Workspace); var snapshot = Snapshot_For_2_1;
projectManager.HostProjectAdded(HostProject_For_2_1);
projectManager.WorkspaceProjectAdded(WorkspaceProject);
var factoryService = new DefaultProjectEngineFactoryService(projectManager, FallbackFactory, CustomFactories); var factory = new DefaultProjectSnapshotProjectEngineFactory(FallbackFactory, CustomFactories);
// Act // Act
var engine = factoryService.Create("/TestPath/SomePath/", b => var engine = factory.Create(snapshot, b =>
{ {
b.Features.Add(new MyCoolNewFeature()); b.Features.Add(new MyCoolNewFeature());
}); });
@ -106,14 +118,12 @@ namespace Microsoft.VisualStudio.Editor.Razor
public void Create_CreatesDesignTimeTemplateEngine_ForVersion2_0() public void Create_CreatesDesignTimeTemplateEngine_ForVersion2_0()
{ {
// Arrange // Arrange
var projectManager = new TestProjectSnapshotManager(Workspace); var snapshot = Snapshot_For_2_0;
projectManager.HostProjectAdded(HostProject_For_2_0);
projectManager.WorkspaceProjectAdded(WorkspaceProject);
var factoryService = new DefaultProjectEngineFactoryService(projectManager, FallbackFactory, CustomFactories); var factory = new DefaultProjectSnapshotProjectEngineFactory(FallbackFactory, CustomFactories);
// Act // Act
var engine = factoryService.Create("/TestPath/SomePath/", b => var engine = factory.Create(snapshot, b =>
{ {
b.Features.Add(new MyCoolNewFeature()); b.Features.Add(new MyCoolNewFeature());
}); });
@ -129,14 +139,12 @@ namespace Microsoft.VisualStudio.Editor.Razor
public void Create_CreatesTemplateEngine_ForVersion1_1() public void Create_CreatesTemplateEngine_ForVersion1_1()
{ {
// Arrange // Arrange
var projectManager = new TestProjectSnapshotManager(Workspace); var snapshot = Snapshot_For_1_1;
projectManager.HostProjectAdded(HostProject_For_1_1);
projectManager.WorkspaceProjectAdded(WorkspaceProject);
var factoryService = new DefaultProjectEngineFactoryService(projectManager, FallbackFactory, CustomFactories); var factory = new DefaultProjectSnapshotProjectEngineFactory(FallbackFactory, CustomFactories);
// Act // Act
var engine = factoryService.Create("/TestPath/SomePath/", b => var engine = factory.Create(snapshot, b =>
{ {
b.Features.Add(new MyCoolNewFeature()); b.Features.Add(new MyCoolNewFeature());
}); });
@ -152,14 +160,12 @@ namespace Microsoft.VisualStudio.Editor.Razor
public void Create_DoesNotSupportViewComponentTagHelpers_ForVersion1_0() public void Create_DoesNotSupportViewComponentTagHelpers_ForVersion1_0()
{ {
// Arrange // Arrange
var projectManager = new TestProjectSnapshotManager(Workspace); var snapshot = Snapshot_For_1_0;
projectManager.HostProjectAdded(HostProject_For_1_0);
projectManager.WorkspaceProjectAdded(WorkspaceProject);
var factoryService = new DefaultProjectEngineFactoryService(projectManager, FallbackFactory, CustomFactories); var factory = new DefaultProjectSnapshotProjectEngineFactory(FallbackFactory, CustomFactories);
// Act // Act
var engine = factoryService.Create("/TestPath/SomePath/", b => var engine = factory.Create(snapshot, b =>
{ {
b.Features.Add(new MyCoolNewFeature()); b.Features.Add(new MyCoolNewFeature());
}); });
@ -171,40 +177,15 @@ namespace Microsoft.VisualStudio.Editor.Razor
Assert.Empty(engine.Engine.Features.OfType<MvcLatest.ViewComponentTagHelperPass>()); 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] [Fact]
public void Create_ForUnknownConfiguration_UsesFallbackFactory() public void Create_ForUnknownConfiguration_UsesFallbackFactory()
{ {
// Arrange var snapshot = Snapshot_For_UnknownConfiguration;
var projectManager = new TestProjectSnapshotManager(Workspace);
projectManager.HostProjectAdded(HostProject_For_UnknownConfiguration);
projectManager.WorkspaceProjectAdded(WorkspaceProject);
var factoryService = new DefaultProjectEngineFactoryService(projectManager, FallbackFactory, CustomFactories); var factory = new DefaultProjectSnapshotProjectEngineFactory(FallbackFactory, CustomFactories);
// Act // Act
var engine = factoryService.Create("/TestPath/SomePath/", b => var engine = factory.Create(snapshot, b =>
{ {
b.Features.Add(new MyCoolNewFeature()); b.Features.Add(new MyCoolNewFeature());
}); });
@ -221,18 +202,5 @@ namespace Microsoft.VisualStudio.Editor.Razor
{ {
public RazorEngine Engine { get; set; } 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;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.Language;
using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Razor; using Microsoft.CodeAnalysis.Razor;
using Microsoft.CodeAnalysis.Razor.Editor; using Microsoft.CodeAnalysis.Razor.Editor;
using Microsoft.CodeAnalysis.Razor.ProjectSystem; using Microsoft.CodeAnalysis.Razor.ProjectSystem;
@ -18,29 +20,90 @@ namespace Microsoft.VisualStudio.Editor.Razor
{ {
public class DefaultVisualStudioDocumentTrackerTest : ForegroundDispatcherTestBase 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() public void EditorSettingsManager_Changed_TriggersContextChanged()
{ {
// Arrange // Arrange
var documentTracker = new DefaultVisualStudioDocumentTracker(Dispatcher, FilePath, ProjectPath, ProjectManager, WorkspaceEditorSettings, Workspace, TextBuffer, ImportDocumentManager);
var called = false; var called = false;
documentTracker.ContextChanged += (sender, args) => DocumentTracker.ContextChanged += (sender, args) =>
{ {
Assert.Equal(ContextChangeKind.EditorSettingsChanged, args.Kind); Assert.Equal(ContextChangeKind.EditorSettingsChanged, args.Kind);
called = true; called = true;
@ -48,103 +111,109 @@ namespace Microsoft.VisualStudio.Editor.Razor
}; };
// Act // Act
documentTracker.EditorSettingsManager_Changed(null, null); DocumentTracker.EditorSettingsManager_Changed(null, null);
// Assert // Assert
Assert.True(called); Assert.True(called);
} }
[Fact] [ForegroundFact]
public void ProjectManager_Changed_ProjectChanged_TriggersContextChanged() public void ProjectManager_Changed_ProjectAdded_TriggersContextChanged()
{ {
// Arrange // Arrange
Project project = null; ProjectManager.HostProjectAdded(HostProject);
var workspace = TestWorkspace.Create(ws => ProjectManager.WorkspaceProjectAdded(WorkspaceProject);
{
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);
var projectSnapshot = new DefaultProjectSnapshot(new HostProject(project.FilePath, RazorConfiguration.Default), project); var e = new ProjectChangeEventArgs(ProjectPath, ProjectChangeKind.ProjectAdded);
var projectChangedArgs = new ProjectChangeEventArgs(projectSnapshot, ProjectChangeKind.Changed);
var called = false; var called = false;
documentTracker.ContextChanged += (sender, args) => DocumentTracker.ContextChanged += (sender, args) =>
{ {
Assert.Equal(ContextChangeKind.ProjectChanged, args.Kind); Assert.Equal(ContextChangeKind.ProjectChanged, args.Kind);
called = true; called = true;
Assert.Same(ProjectManager.GetLoadedProject(DocumentTracker.ProjectPath), DocumentTracker.ProjectSnapshot);
}; };
// Act // Act
documentTracker.ProjectManager_Changed(null, projectChangedArgs); DocumentTracker.ProjectManager_Changed(ProjectManager, e);
// Assert // Assert
Assert.True(called); Assert.True(called);
} }
[Fact] [ForegroundFact]
public void ProjectManager_Changed_TagHelpersChanged_TriggersContextChanged() public void ProjectManager_Changed_ProjectChanged_TriggersContextChanged()
{ {
// Arrange // Arrange
Project project = null; ProjectManager.HostProjectAdded(HostProject);
var workspace = TestWorkspace.Create(ws => ProjectManager.WorkspaceProjectAdded(WorkspaceProject);
{
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);
var projectSnapshot = new DefaultProjectSnapshot(new HostProject(project.FilePath, RazorConfiguration.Default), project); var e = new ProjectChangeEventArgs(ProjectPath, ProjectChangeKind.ProjectChanged);
var projectChangedArgs = new ProjectChangeEventArgs(projectSnapshot, ProjectChangeKind.TagHelpersChanged);
var called = false; 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; called = true;
Assert.Same(ProjectManager.GetLoadedProject(DocumentTracker.ProjectPath), DocumentTracker.ProjectSnapshot);
}; };
// Act // Act
documentTracker.ProjectManager_Changed(null, projectChangedArgs); DocumentTracker.ProjectManager_Changed(ProjectManager, e);
// Assert // Assert
Assert.True(called); 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() public void ProjectManager_Changed_IgnoresUnknownProject()
{ {
// Arrange // Arrange
Project project = null; var e = new ProjectChangeEventArgs("c:/OtherPath/OtherProject.csproj", ProjectChangeKind.ProjectChanged);
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 called = false; var called = false;
documentTracker.ContextChanged += (sender, args) => DocumentTracker.ContextChanged += (sender, args) =>
{ {
called = true; called = true;
}; };
// Act // Act
documentTracker.ProjectManager_Changed(null, projectChangedArgs); DocumentTracker.ProjectManager_Changed(ProjectManager, e);
// Assert // Assert
Assert.False(called); Assert.False(called);
} }
[Fact] [ForegroundFact]
public void Import_Changed_ImportAssociatedWithDocument_TriggersContextChanged() public void Import_Changed_ImportAssociatedWithDocument_TriggersContextChanged()
{ {
// Arrange // Arrange
var documentTracker = new DefaultVisualStudioDocumentTracker(Dispatcher, FilePath, ProjectPath, ProjectManager, WorkspaceEditorSettings, Workspace, TextBuffer, ImportDocumentManager);
var called = false; var called = false;
documentTracker.ContextChanged += (sender, args) => DocumentTracker.ContextChanged += (sender, args) =>
{ {
Assert.Equal(ContextChangeKind.ImportsChanged, args.Kind); Assert.Equal(ContextChangeKind.ImportsChanged, args.Kind);
called = true; called = true;
@ -153,19 +222,17 @@ namespace Microsoft.VisualStudio.Editor.Razor
var importChangedArgs = new ImportChangedEventArgs("path/to/import", FileChangeKind.Changed, new[] { FilePath }); var importChangedArgs = new ImportChangedEventArgs("path/to/import", FileChangeKind.Changed, new[] { FilePath });
// Act // Act
documentTracker.Import_Changed(null, importChangedArgs); DocumentTracker.Import_Changed(null, importChangedArgs);
// Assert // Assert
Assert.True(called); Assert.True(called);
} }
[Fact] [ForegroundFact]
public void Import_Changed_UnrelatedImport_DoesNothing() public void Import_Changed_UnrelatedImport_DoesNothing()
{ {
// Arrange // Arrange
var documentTracker = new DefaultVisualStudioDocumentTracker(Dispatcher, FilePath, ProjectPath, ProjectManager, WorkspaceEditorSettings, Workspace, TextBuffer, ImportDocumentManager); DocumentTracker.ContextChanged += (sender, args) =>
documentTracker.ContextChanged += (sender, args) =>
{ {
throw new InvalidOperationException(); 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" }); var importChangedArgs = new ImportChangedEventArgs("path/to/import", FileChangeKind.Changed, new[] { "path/to/differentfile" });
// Act & Assert (Does not throw) // Act & Assert (Does not throw)
documentTracker.Import_Changed(null, importChangedArgs); DocumentTracker.Import_Changed(null, importChangedArgs);
} }
[Fact] [ForegroundFact]
public void Subscribe_SetsSupportedProjectAndTriggersContextChanged() public void Subscribe_SetsSupportedProjectAndTriggersContextChanged()
{ {
// Arrange // Arrange
var documentTracker = new DefaultVisualStudioDocumentTracker(Dispatcher, FilePath, ProjectPath, ProjectManager, WorkspaceEditorSettings, Workspace, TextBuffer, ImportDocumentManager);
var called = false; var called = false;
documentTracker.ContextChanged += (sender, args) => DocumentTracker.ContextChanged += (sender, args) =>
{ {
called = true; called = true; // This will trigger both ContextChanged and TagHelprsChanged
Assert.Equal(ContextChangeKind.ProjectChanged, args.Kind);
}; };
// Act // Act
documentTracker.Subscribe(); DocumentTracker.Subscribe();
// Assert // Assert
Assert.True(called); Assert.True(called);
Assert.True(documentTracker.IsSupportedProject); Assert.True(DocumentTracker.IsSupportedProject);
} }
[Fact] [ForegroundFact]
public void Unsubscribe_ResetsSupportedProjectAndTriggersContextChanged() public void Unsubscribe_ResetsSupportedProjectAndTriggersContextChanged()
{ {
// Arrange // Arrange
var documentTracker = new DefaultVisualStudioDocumentTracker(Dispatcher, FilePath, ProjectPath, ProjectManager, WorkspaceEditorSettings, Workspace, TextBuffer, ImportDocumentManager);
// Subscribe once to set supported project // Subscribe once to set supported project
documentTracker.Subscribe(); DocumentTracker.Subscribe();
var called = false; var called = false;
documentTracker.ContextChanged += (sender, args) => DocumentTracker.ContextChanged += (sender, args) =>
{ {
called = true; called = true;
Assert.Equal(ContextChangeKind.ProjectChanged, args.Kind); Assert.Equal(ContextChangeKind.ProjectChanged, args.Kind);
}; };
// Act // Act
documentTracker.Unsubscribe(); DocumentTracker.Unsubscribe();
// Assert // Assert
Assert.False(documentTracker.IsSupportedProject); Assert.False(DocumentTracker.IsSupportedProject);
Assert.True(called); Assert.True(called);
} }
[Fact] [ForegroundFact]
public void AddTextView_AddsToTextViewCollection() public void AddTextView_AddsToTextViewCollection()
{ {
// Arrange // Arrange
var documentTracker = new DefaultVisualStudioDocumentTracker(Dispatcher, FilePath, ProjectPath, ProjectManager, WorkspaceEditorSettings, Workspace, TextBuffer, ImportDocumentManager);
var textView = Mock.Of<ITextView>(); var textView = Mock.Of<ITextView>();
// Act // Act
documentTracker.AddTextView(textView); DocumentTracker.AddTextView(textView);
// Assert // 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() public void AddTextView_DoesNotAddDuplicateTextViews()
{ {
// Arrange // Arrange
var documentTracker = new DefaultVisualStudioDocumentTracker(Dispatcher, FilePath, ProjectPath, ProjectManager, WorkspaceEditorSettings, Workspace, TextBuffer, ImportDocumentManager);
var textView = Mock.Of<ITextView>(); var textView = Mock.Of<ITextView>();
// Act // Act
documentTracker.AddTextView(textView); DocumentTracker.AddTextView(textView);
documentTracker.AddTextView(textView); DocumentTracker.AddTextView(textView);
// Assert // 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() public void AddTextView_AddsMultipleTextViewsToCollection()
{ {
// Arrange // Arrange
var documentTracker = new DefaultVisualStudioDocumentTracker(Dispatcher, FilePath, ProjectPath, ProjectManager, WorkspaceEditorSettings, Workspace, TextBuffer, ImportDocumentManager);
var textView1 = Mock.Of<ITextView>(); var textView1 = Mock.Of<ITextView>();
var textView2 = Mock.Of<ITextView>(); var textView2 = Mock.Of<ITextView>();
// Act // Act
documentTracker.AddTextView(textView1); DocumentTracker.AddTextView(textView1);
documentTracker.AddTextView(textView2); DocumentTracker.AddTextView(textView2);
// Assert // Assert
Assert.Collection( Assert.Collection(
documentTracker.TextViews, DocumentTracker.TextViews,
v => Assert.Same(v, textView1), v => Assert.Same(v, textView1),
v => Assert.Same(v, textView2)); v => Assert.Same(v, textView2));
} }
[Fact] [ForegroundFact]
public void RemoveTextView_RemovesTextViewFromCollection_SingleItem() public void RemoveTextView_RemovesTextViewFromCollection_SingleItem()
{ {
// Arrange // Arrange
var documentTracker = new DefaultVisualStudioDocumentTracker(Dispatcher, FilePath, ProjectPath, ProjectManager, WorkspaceEditorSettings, Workspace, TextBuffer, ImportDocumentManager);
var textView = Mock.Of<ITextView>(); var textView = Mock.Of<ITextView>();
documentTracker.AddTextView(textView); DocumentTracker.AddTextView(textView);
// Act // Act
documentTracker.RemoveTextView(textView); DocumentTracker.RemoveTextView(textView);
// Assert // Assert
Assert.Empty(documentTracker.TextViews); Assert.Empty(DocumentTracker.TextViews);
} }
[Fact] [ForegroundFact]
public void RemoveTextView_RemovesTextViewFromCollection_MultipleItems() public void RemoveTextView_RemovesTextViewFromCollection_MultipleItems()
{ {
// Arrange // Arrange
var documentTracker = new DefaultVisualStudioDocumentTracker(Dispatcher, FilePath, ProjectPath, ProjectManager, WorkspaceEditorSettings, Workspace, TextBuffer, ImportDocumentManager);
var textView1 = Mock.Of<ITextView>(); var textView1 = Mock.Of<ITextView>();
var textView2 = Mock.Of<ITextView>(); var textView2 = Mock.Of<ITextView>();
var textView3 = Mock.Of<ITextView>(); var textView3 = Mock.Of<ITextView>();
documentTracker.AddTextView(textView1); DocumentTracker.AddTextView(textView1);
documentTracker.AddTextView(textView2); DocumentTracker.AddTextView(textView2);
documentTracker.AddTextView(textView3); DocumentTracker.AddTextView(textView3);
// Act // Act
documentTracker.RemoveTextView(textView2); DocumentTracker.RemoveTextView(textView2);
// Assert // Assert
Assert.Collection( Assert.Collection(
documentTracker.TextViews, DocumentTracker.TextViews,
v => Assert.Same(v, textView1), v => Assert.Same(v, textView1),
v => Assert.Same(v, textView3)); v => Assert.Same(v, textView3));
} }
[Fact] [ForegroundFact]
public void RemoveTextView_NoopsWhenRemovingTextViewNotInCollection() public void RemoveTextView_NoopsWhenRemovingTextViewNotInCollection()
{ {
// Arrange // Arrange
var documentTracker = new DefaultVisualStudioDocumentTracker(Dispatcher, FilePath, ProjectPath, ProjectManager, WorkspaceEditorSettings, Workspace, TextBuffer, ImportDocumentManager);
var textView1 = Mock.Of<ITextView>(); var textView1 = Mock.Of<ITextView>();
documentTracker.AddTextView(textView1); DocumentTracker.AddTextView(textView1);
var textView2 = Mock.Of<ITextView>(); var textView2 = Mock.Of<ITextView>();
// Act // Act
documentTracker.RemoveTextView(textView2); DocumentTracker.RemoveTextView(textView2);
// Assert // 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;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Razor.Extensions; using Microsoft.AspNetCore.Mvc.Razor.Extensions;
using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.Language.Legacy; using Microsoft.AspNetCore.Razor.Language.Legacy;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Razor; using Microsoft.CodeAnalysis.Razor;
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
using Microsoft.VisualStudio.Test; using Microsoft.VisualStudio.Test;
using Microsoft.VisualStudio.Text; using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Editor; 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 TestLinePragmaFileName = "C:\\This\\Path\\Is\\Just\\For\\Line\\Pragmas.cshtml";
private const string TestProjectPath = "C:\\This\\Path\\Is\\Just\\For\\Project.csproj"; 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] [ForegroundFact]
public async Task BufferChangeStartsFullReparseIfChangeOverlapsMultipleSpans() public async Task BufferChangeStartsFullReparseIfChangeOverlapsMultipleSpans()
{ {
@ -505,7 +516,7 @@ namespace Microsoft.VisualStudio.Editor.Razor
{ {
var textBuffer = new TestTextBuffer(originalSnapshot); var textBuffer = new TestTextBuffer(originalSnapshot);
var documentTracker = CreateDocumentTracker(textBuffer); var documentTracker = CreateDocumentTracker(textBuffer);
var templateEngineFactory = CreateTemplateEngineFactory(); var templateEngineFactory = CreateProjectEngineFactory();
var parser = new DefaultVisualStudioRazorParser( var parser = new DefaultVisualStudioRazorParser(
Dispatcher, Dispatcher,
documentTracker, documentTracker,
@ -524,7 +535,7 @@ namespace Microsoft.VisualStudio.Editor.Razor
return new TestParserManager(parser); return new TestParserManager(parser);
} }
private static RazorProjectEngineFactoryService CreateTemplateEngineFactory( private static ProjectSnapshotProjectEngineFactory CreateProjectEngineFactory(
string path = TestLinePragmaFileName, string path = TestLinePragmaFileName,
IEnumerable<TagHelperDescriptor> tagHelpers = null) IEnumerable<TagHelperDescriptor> tagHelpers = null)
{ {
@ -541,10 +552,10 @@ namespace Microsoft.VisualStudio.Editor.Razor
} }
}); });
var projectEngineFactoryService = Mock.Of<RazorProjectEngineFactoryService>( return new TestProjectSnapshotProjectEngineFactory()
service => service.Create(It.IsAny<string>(), It.IsAny<Action<RazorProjectEngineBuilder>>()) == projectEngine); {
Engine = projectEngine,
return projectEngineFactoryService; };
} }
private async Task RunTypeKeywordTestAsync(string keyword) private async Task RunTypeKeywordTestAsync(string keyword)
@ -584,7 +595,7 @@ namespace Microsoft.VisualStudio.Editor.Razor
#endif #endif
} }
private static VisualStudioDocumentTracker CreateDocumentTracker(Text.ITextBuffer textBuffer) private VisualStudioDocumentTracker CreateDocumentTracker(Text.ITextBuffer textBuffer)
{ {
var focusedTextView = Mock.Of<ITextView>(textView => textView.HasAggregateFocus == true); var focusedTextView = Mock.Of<ITextView>(textView => textView.HasAggregateFocus == true);
var documentTracker = Mock.Of<VisualStudioDocumentTracker>(tracker => var documentTracker = Mock.Of<VisualStudioDocumentTracker>(tracker =>
@ -592,6 +603,7 @@ namespace Microsoft.VisualStudio.Editor.Razor
tracker.TextViews == new[] { focusedTextView } && tracker.TextViews == new[] { focusedTextView } &&
tracker.FilePath == TestLinePragmaFileName && tracker.FilePath == TestLinePragmaFileName &&
tracker.ProjectPath == TestProjectPath && tracker.ProjectPath == TestProjectPath &&
tracker.ProjectSnapshot == ProjectSnapshot &&
tracker.IsSupportedProject == true); tracker.IsSupportedProject == true);
textBuffer.Properties.AddProperty(typeof(VisualStudioDocumentTracker), documentTracker); textBuffer.Properties.AddProperty(typeof(VisualStudioDocumentTracker), documentTracker);

View File

@ -4,7 +4,9 @@
using System; using System;
using System.Threading; using System.Threading;
using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.Language;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Razor; using Microsoft.CodeAnalysis.Razor;
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
using Microsoft.VisualStudio.Test; using Microsoft.VisualStudio.Test;
using Microsoft.VisualStudio.Text; using Microsoft.VisualStudio.Text;
using Moq; using Moq;
@ -14,12 +16,32 @@ namespace Microsoft.VisualStudio.Editor.Razor
{ {
public class DefaultVisualStudioRazorParserTest : ForegroundDispatcherTestBase 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 => var documentTracker = Mock.Of<VisualStudioDocumentTracker>(tracker =>
tracker.TextBuffer == new TestTextBuffer(new StringTextSnapshot(string.Empty)) && tracker.TextBuffer == new TestTextBuffer(new StringTextSnapshot(string.Empty)) &&
tracker.ProjectPath == "SomeProject.csproj" && tracker.ProjectPath == "c:\\SomeProject.csproj" &&
tracker.FilePath == "SomeFilePath.cshtml" && tracker.ProjectSnapshot == ProjectSnapshot &&
tracker.FilePath == "c:\\SomeFilePath.cshtml" &&
tracker.IsSupportedProject == isSupportedProject); tracker.IsSupportedProject == isSupportedProject);
return documentTracker; return documentTracker;
@ -32,7 +54,7 @@ namespace Microsoft.VisualStudio.Editor.Razor
var parser = new DefaultVisualStudioRazorParser( var parser = new DefaultVisualStudioRazorParser(
Dispatcher, Dispatcher,
CreateDocumentTracker(), CreateDocumentTracker(),
Mock.Of<RazorProjectEngineFactoryService>(), ProjectEngineFactory,
new DefaultErrorReporter(), new DefaultErrorReporter(),
Mock.Of<VisualStudioCompletionBroker>()); Mock.Of<VisualStudioCompletionBroker>());
parser.Dispose(); parser.Dispose();
@ -48,7 +70,7 @@ namespace Microsoft.VisualStudio.Editor.Razor
var parser = new DefaultVisualStudioRazorParser( var parser = new DefaultVisualStudioRazorParser(
Dispatcher, Dispatcher,
CreateDocumentTracker(), CreateDocumentTracker(),
Mock.Of<RazorProjectEngineFactoryService>(), ProjectEngineFactory,
new DefaultErrorReporter(), new DefaultErrorReporter(),
Mock.Of<VisualStudioCompletionBroker>()); Mock.Of<VisualStudioCompletionBroker>());
parser.Dispose(); parser.Dispose();
@ -64,7 +86,7 @@ namespace Microsoft.VisualStudio.Editor.Razor
var parser = new DefaultVisualStudioRazorParser( var parser = new DefaultVisualStudioRazorParser(
Dispatcher, Dispatcher,
CreateDocumentTracker(), CreateDocumentTracker(),
Mock.Of<RazorProjectEngineFactoryService>(), ProjectEngineFactory,
new DefaultErrorReporter(), new DefaultErrorReporter(),
Mock.Of<VisualStudioCompletionBroker>()); Mock.Of<VisualStudioCompletionBroker>());
parser.Dispose(); parser.Dispose();
@ -80,7 +102,7 @@ namespace Microsoft.VisualStudio.Editor.Razor
using (var parser = new DefaultVisualStudioRazorParser( using (var parser = new DefaultVisualStudioRazorParser(
Dispatcher, Dispatcher,
CreateDocumentTracker(), CreateDocumentTracker(),
Mock.Of<RazorProjectEngineFactoryService>(), ProjectEngineFactory,
new DefaultErrorReporter(), new DefaultErrorReporter(),
Mock.Of<VisualStudioCompletionBroker>())) Mock.Of<VisualStudioCompletionBroker>()))
{ {
@ -108,7 +130,7 @@ namespace Microsoft.VisualStudio.Editor.Razor
using (var parser = new DefaultVisualStudioRazorParser( using (var parser = new DefaultVisualStudioRazorParser(
Dispatcher, Dispatcher,
documentTracker, documentTracker,
Mock.Of<RazorProjectEngineFactoryService>(), ProjectEngineFactory,
new DefaultErrorReporter(), new DefaultErrorReporter(),
Mock.Of<VisualStudioCompletionBroker>())) Mock.Of<VisualStudioCompletionBroker>()))
{ {
@ -139,7 +161,7 @@ namespace Microsoft.VisualStudio.Editor.Razor
using (var parser = new DefaultVisualStudioRazorParser( using (var parser = new DefaultVisualStudioRazorParser(
Dispatcher, Dispatcher,
CreateDocumentTracker(), CreateDocumentTracker(),
Mock.Of<RazorProjectEngineFactoryService>(), ProjectEngineFactory,
new DefaultErrorReporter(), new DefaultErrorReporter(),
Mock.Of<VisualStudioCompletionBroker>()) Mock.Of<VisualStudioCompletionBroker>())
{ {
@ -169,7 +191,7 @@ namespace Microsoft.VisualStudio.Editor.Razor
using (var parser = new DefaultVisualStudioRazorParser( using (var parser = new DefaultVisualStudioRazorParser(
Dispatcher, Dispatcher,
CreateDocumentTracker(), CreateDocumentTracker(),
Mock.Of<RazorProjectEngineFactoryService>(), ProjectEngineFactory,
new DefaultErrorReporter(), new DefaultErrorReporter(),
Mock.Of<VisualStudioCompletionBroker>()) Mock.Of<VisualStudioCompletionBroker>())
{ {
@ -198,7 +220,7 @@ namespace Microsoft.VisualStudio.Editor.Razor
using (var parser = new DefaultVisualStudioRazorParser( using (var parser = new DefaultVisualStudioRazorParser(
Dispatcher, Dispatcher,
CreateDocumentTracker(), CreateDocumentTracker(),
Mock.Of<RazorProjectEngineFactoryService>(), ProjectEngineFactory,
new DefaultErrorReporter(), new DefaultErrorReporter(),
Mock.Of<VisualStudioCompletionBroker>())) Mock.Of<VisualStudioCompletionBroker>()))
{ {
@ -222,7 +244,7 @@ namespace Microsoft.VisualStudio.Editor.Razor
using (var parser = new DefaultVisualStudioRazorParser( using (var parser = new DefaultVisualStudioRazorParser(
Dispatcher, Dispatcher,
documentTracker, documentTracker,
Mock.Of<RazorProjectEngineFactoryService>(), ProjectEngineFactory,
new DefaultErrorReporter(), new DefaultErrorReporter(),
Mock.Of<VisualStudioCompletionBroker>())) Mock.Of<VisualStudioCompletionBroker>()))
{ {
@ -242,7 +264,7 @@ namespace Microsoft.VisualStudio.Editor.Razor
using (var parser = new DefaultVisualStudioRazorParser( using (var parser = new DefaultVisualStudioRazorParser(
Dispatcher, Dispatcher,
CreateDocumentTracker(isSupportedProject: true), CreateDocumentTracker(isSupportedProject: true),
Mock.Of<RazorProjectEngineFactoryService>(), ProjectEngineFactory,
new DefaultErrorReporter(), new DefaultErrorReporter(),
Mock.Of<VisualStudioCompletionBroker>())) Mock.Of<VisualStudioCompletionBroker>()))
{ {
@ -261,7 +283,7 @@ namespace Microsoft.VisualStudio.Editor.Razor
using (var parser = new DefaultVisualStudioRazorParser( using (var parser = new DefaultVisualStudioRazorParser(
Dispatcher, Dispatcher,
CreateDocumentTracker(isSupportedProject: false), CreateDocumentTracker(isSupportedProject: false),
Mock.Of<RazorProjectEngineFactoryService>(), ProjectEngineFactory,
new DefaultErrorReporter(), new DefaultErrorReporter(),
Mock.Of<VisualStudioCompletionBroker>())) Mock.Of<VisualStudioCompletionBroker>()))
{ {

View File

@ -5,6 +5,12 @@
<PreserveCompilationContext>true</PreserveCompilationContext> <PreserveCompilationContext>true</PreserveCompilationContext>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<Compile Include="..\Microsoft.CodeAnalysis.Razor.Workspaces.Test\Shared\**\*.cs">
<Link>Shared\%(RecursiveDir)%(FileName)%(Extension)</Link>
</Compile>
</ItemGroup>
<ItemGroup> <ItemGroup>
<None Include="xunit.runner.json" CopyToOutputDirectory="PreserveNewest" /> <None Include="xunit.runner.json" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup> </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 // 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. // 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); Documents = new HostDocument[]
HostProject2 = new HostProject("Test2.csproj", FallbackRazorConfiguration.MVC_1_0); {
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(); Workspace = TestWorkspace.Create();
@ -31,19 +37,21 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
"Test1", "Test1",
"Test1", "Test1",
LanguageNames.CSharp, LanguageNames.CSharp,
"Test1.csproj")) "c:\\Test1\\Test1.csproj"))
.AddProject(ProjectInfo.Create( .AddProject(ProjectInfo.Create(
projectId2, projectId2,
VersionStamp.Default, VersionStamp.Default,
"Test2", "Test2",
"Test2", "Test2",
LanguageNames.CSharp, LanguageNames.CSharp,
"Test2.csproj")); ; "c:\\Test2\\Test2.csproj")); ;
WorkspaceProject1 = solution.GetProject(projectId1); WorkspaceProject1 = solution.GetProject(projectId1);
WorkspaceProject2 = solution.GetProject(projectId2); WorkspaceProject2 = solution.GetProject(projectId2);
} }
private HostDocument[] Documents { get; }
private HostProject HostProject1 { get; } private HostProject HostProject1 { get; }
private HostProject HostProject2 { get; } private HostProject HostProject2 { get; }
@ -63,28 +71,31 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
projectManager.HostProjectAdded(HostProject2); projectManager.HostProjectAdded(HostProject2);
projectManager.WorkspaceProjectAdded(WorkspaceProject1); projectManager.WorkspaceProjectAdded(WorkspaceProject1);
projectManager.WorkspaceProjectAdded(WorkspaceProject2); 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), Delay = TimeSpan.FromMilliseconds(1),
BlockBackgroundWorkStart = new ManualResetEventSlim(initialState: false), BlockBackgroundWorkStart = new ManualResetEventSlim(initialState: false),
NotifyBackgroundWorkFinish = new ManualResetEventSlim(initialState: false), NotifyBackgroundWorkStarting = new ManualResetEventSlim(initialState: false),
NotifyForegroundWorkFinish = new ManualResetEventSlim(initialState: false), BlockBackgroundWorkCompleting = new ManualResetEventSlim(initialState: false),
NotifyBackgroundWorkCompleted = new ManualResetEventSlim(initialState: false),
}; };
// Act & Assert // 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.IsScheduledOrRunning, "Queue should be scheduled during Enqueue");
Assert.True(queue.HasPendingNotifications, "Queue should have a notification created 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 proceed.
queue.BlockBackgroundWorkStart.Set(); queue.BlockBackgroundWorkStart.Set();
queue.BlockBackgroundWorkCompleting.Set();
// Get off the foreground thread and allow the updates to flow through. await Task.Run(() => queue.NotifyBackgroundWorkCompleted.Wait(TimeSpan.FromSeconds(1)));
await Task.Run(() => queue.NotifyForegroundWorkFinish.Wait(TimeSpan.FromSeconds(1)));
Assert.False(queue.IsScheduledOrRunning, "Queue should not have restarted"); Assert.False(queue.IsScheduledOrRunning, "Queue should not have restarted");
Assert.False(queue.HasPendingNotifications, "Queue should have processed all notifications"); Assert.False(queue.HasPendingNotifications, "Queue should have processed all notifications");
@ -99,40 +110,42 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
projectManager.HostProjectAdded(HostProject2); projectManager.HostProjectAdded(HostProject2);
projectManager.WorkspaceProjectAdded(WorkspaceProject1); projectManager.WorkspaceProjectAdded(WorkspaceProject1);
projectManager.WorkspaceProjectAdded(WorkspaceProject2); 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), Delay = TimeSpan.FromMilliseconds(1),
BlockBackgroundWorkStart = new ManualResetEventSlim(initialState: false), BlockBackgroundWorkStart = new ManualResetEventSlim(initialState: false),
NotifyBackgroundWorkFinish = new ManualResetEventSlim(initialState: false), NotifyBackgroundWorkStarting = new ManualResetEventSlim(initialState: false),
NotifyForegroundWorkFinish = new ManualResetEventSlim(initialState: false), BlockBackgroundWorkCompleting = new ManualResetEventSlim(initialState: false),
NotifyBackgroundWorkCompleted = new ManualResetEventSlim(initialState: false),
}; };
// Act & Assert // 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.IsScheduledOrRunning, "Queue should be scheduled during Enqueue");
Assert.True(queue.HasPendingNotifications, "Queue should have a notification created 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.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.True(queue.IsScheduledOrRunning, "Worker should be processing now");
Assert.False(queue.HasPendingNotifications, "Worker should have taken all notifications"); 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. 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. // Allow work to complete, which should restart the timer.
await Task.Run(() => queue.NotifyForegroundWorkFinish.Wait(TimeSpan.FromSeconds(1))); queue.BlockBackgroundWorkCompleting.Set();
queue.NotifyBackgroundWorkFinish.Reset(); await Task.Run(() => queue.NotifyBackgroundWorkCompleted.Wait(TimeSpan.FromSeconds(1)));
queue.NotifyForegroundWorkFinish.Reset(); queue.NotifyBackgroundWorkCompleted.Reset();
// It should start running again right away. // It should start running again right away.
Assert.True(queue.IsScheduledOrRunning, "Queue should be scheduled during Enqueue"); 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. // Allow the background work to proceed.
queue.BlockBackgroundWorkStart.Set(); queue.BlockBackgroundWorkStart.Set();
// Get off the foreground thread and allow the updates to flow through. queue.BlockBackgroundWorkCompleting.Set();
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.IsScheduledOrRunning, "Queue should not have restarted");
Assert.False(queue.HasPendingNotifications, "Queue should have processed all notifications"); 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> <GenerateBindingRedirectsOutputType>true</GenerateBindingRedirectsOutputType>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<Compile Include="..\Microsoft.CodeAnalysis.Razor.Workspaces.Test\Shared\**\*.cs">
<Link>Shared\%(RecursiveDir)%(FileName)%(Extension)</Link>
</Compile>
</ItemGroup>
<ItemGroup> <ItemGroup>
<None Include="xunit.runner.json" CopyToOutputDirectory="PreserveNewest" /> <None Include="xunit.runner.json" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup> </ItemGroup>

View File

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

View File

@ -1,8 +1,12 @@
// Copyright (c) .NET Foundation. All rights reserved. // Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.CodeAnalysis.Host;
using Moq; using Moq;
using Xunit; using Xunit;
@ -12,9 +16,31 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
{ {
public DefaultProjectSnapshotManagerTest() 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); ProjectManager = new TestProjectSnapshotManager(Dispatcher, Enumerable.Empty<ProjectSnapshotChangeTrigger>(), Workspace);
var projectId = ProjectId.CreateNewId("Test"); var projectId = ProjectId.CreateNewId("Test");
@ -24,7 +50,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
"Test", "Test",
"Test", "Test",
LanguageNames.CSharp, LanguageNames.CSharp,
"Test.csproj")); "c:\\MyProject\\Test.csproj"));
WorkspaceProject = solution.GetProject(projectId); WorkspaceProject = solution.GetProject(projectId);
var vbProjectId = ProjectId.CreateNewId("VB"); var vbProjectId = ProjectId.CreateNewId("VB");
@ -54,12 +80,19 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
"Test (Different TFM)", "Test (Different TFM)",
"Test", "Test",
LanguageNames.CSharp, LanguageNames.CSharp,
"Test.csproj")); "c:\\MyProject\\Test.csproj"));
WorkspaceProjectWithDifferentTfm = solution.GetProject(projectIdWithDifferentTfm); WorkspaceProjectWithDifferentTfm = solution.GetProject(projectIdWithDifferentTfm);
SomeTagHelpers = TagHelperResolver.TagHelpers;
SomeTagHelpers.Add(TagHelperDescriptorBuilder.Create("Test1", "TestAssembly").Build());
} }
private HostDocument[] Documents { get; }
private HostProject HostProject { get; } private HostProject HostProject { get; }
private HostProject HostProjectWithConfigurationChange { get; }
private Project WorkspaceProject { get; } private Project WorkspaceProject { get; }
private Project WorkspaceProjectWithDifferentTfm { get; } private Project WorkspaceProjectWithDifferentTfm { get; }
@ -68,30 +101,203 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
private Project VBWorkspaceProject { get; } private Project VBWorkspaceProject { get; }
private TestTagHelperResolver TagHelperResolver { get; }
private TestProjectSnapshotManager ProjectManager { get; } private TestProjectSnapshotManager ProjectManager { get; }
private HostServices HostServices { get; }
private Workspace Workspace { get; } private Workspace Workspace { get; }
private IList<TagHelperDescriptor> SomeTagHelpers { get; }
[ForegroundFact] [ForegroundFact]
public void HostProjectBuildComplete_FindsChangedWorkspaceProject_AndStartsBackgroundWorker() public void DocumentAdded_AddsDocument()
{ {
// Arrange // Arrange
Assert.True(Workspace.TryApplyChanges(WorkspaceProject.Solution));
ProjectManager.HostProjectAdded(HostProject); ProjectManager.HostProjectAdded(HostProject);
var project = WorkspaceProject.WithAssemblyName("Test1"); // Simulate a project change ProjectManager.WorkspaceProjectAdded(WorkspaceProject);
ProjectManager.WorkspaceProjectAdded(project);
ProjectManager.Reset(); ProjectManager.Reset();
// Act // Act
ProjectManager.HostProjectBuildComplete(HostProject); ProjectManager.DocumentAdded(HostProject, Documents[0]);
// Assert // Assert
var snapshot = ProjectManager.GetSnapshot(HostProject); var snapshot = ProjectManager.GetSnapshot(HostProject);
Assert.True(snapshot.IsDirty); Assert.Collection(snapshot.DocumentFilePaths, d => Assert.Equal(Documents[0].FilePath, d));
Assert.True(snapshot.IsInitialized);
Assert.False(ProjectManager.ListenersNotified); Assert.Equal(ProjectChangeKind.DocumentsChanged, ProjectManager.ListenersNotifiedOf);
Assert.True(ProjectManager.WorkerStarted); }
[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] [ForegroundFact]
@ -104,15 +310,13 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
// Assert // Assert
var snapshot = ProjectManager.GetSnapshot(HostProject); var snapshot = ProjectManager.GetSnapshot(HostProject);
Assert.True(snapshot.IsDirty);
Assert.False(snapshot.IsInitialized); Assert.False(snapshot.IsInitialized);
Assert.True(ProjectManager.ListenersNotified); Assert.Equal(ProjectChangeKind.ProjectAdded, ProjectManager.ListenersNotifiedOf);
Assert.False(ProjectManager.WorkerStarted);
} }
[ForegroundFact] [ForegroundFact]
public void HostProjectAdded_FindsWorkspaceProject_NotifiesListeners_AndStartsBackgroundWorker() public void HostProjectAdded_FindsWorkspaceProject_NotifiesListeners()
{ {
// Arrange // Arrange
Assert.True(Workspace.TryApplyChanges(WorkspaceProject.Solution)); Assert.True(Workspace.TryApplyChanges(WorkspaceProject.Solution));
@ -122,60 +326,85 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
// Assert // Assert
var snapshot = ProjectManager.GetSnapshot(HostProject); var snapshot = ProjectManager.GetSnapshot(HostProject);
Assert.True(snapshot.IsDirty);
Assert.True(snapshot.IsInitialized); Assert.True(snapshot.IsInitialized);
Assert.True(ProjectManager.ListenersNotified); Assert.Equal(ProjectChangeKind.ProjectAdded, ProjectManager.ListenersNotifiedOf);
Assert.True(ProjectManager.WorkerStarted);
} }
[ForegroundFact] [ForegroundFact]
public void HostProjectChanged_WithoutWorkspaceProject_NotifiesListeners_AndDoesNotStartBackgroundWorker() public void HostProjectChanged_ConfigurationChange_WithoutWorkspaceProject_NotifiesListeners()
{ {
// Arrange // Arrange
ProjectManager.HostProjectAdded(HostProject); ProjectManager.HostProjectAdded(HostProject);
ProjectManager.Reset(); ProjectManager.Reset();
var project = new HostProject(HostProject.FilePath, FallbackRazorConfiguration.MVC_1_0); // Simulate a project change
// Act // Act
ProjectManager.HostProjectChanged(project); ProjectManager.HostProjectChanged(HostProjectWithConfigurationChange);
// Assert // Assert
var snapshot = ProjectManager.GetSnapshot(HostProject); var snapshot = ProjectManager.GetSnapshot(HostProjectWithConfigurationChange);
Assert.True(snapshot.IsDirty);
Assert.False(snapshot.IsInitialized); Assert.False(snapshot.IsInitialized);
Assert.True(ProjectManager.ListenersNotified); Assert.Equal(ProjectChangeKind.ProjectChanged, ProjectManager.ListenersNotifiedOf);
Assert.False(ProjectManager.WorkerStarted);
} }
[ForegroundFact] [ForegroundFact]
public void HostProjectChanged_WithWorkspaceProject_RetainsComputedState_NotifiesListeners_AndStartsBackgroundWorker() public void HostProjectChanged_ConfigurationChange_WithWorkspaceProject_NotifiesListeners()
{ {
// Arrange // Arrange
ProjectManager.HostProjectAdded(HostProject); ProjectManager.HostProjectAdded(HostProject);
ProjectManager.WorkspaceProjectAdded(WorkspaceProject); ProjectManager.WorkspaceProjectAdded(WorkspaceProject);
ProjectManager.Reset(); 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 // Act
ProjectManager.HostProjectChanged(project); ProjectManager.HostProjectChanged(HostProjectWithConfigurationChange);
// Assert // Assert
snapshot = ProjectManager.GetSnapshot(project); var snapshot = ProjectManager.GetSnapshot(HostProjectWithConfigurationChange);
Assert.True(snapshot.IsDirty);
Assert.True(snapshot.IsInitialized); Assert.True(snapshot.IsInitialized);
Assert.True(ProjectManager.ListenersNotified); Assert.Equal(ProjectChangeKind.ProjectChanged, ProjectManager.ListenersNotifiedOf);
Assert.True(ProjectManager.WorkerStarted); }
[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] [ForegroundFact]
@ -189,8 +418,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
// Assert // Assert
Assert.Empty(ProjectManager.Projects); Assert.Empty(ProjectManager.Projects);
Assert.False(ProjectManager.ListenersNotified); Assert.Null(ProjectManager.ListenersNotifiedOf);
Assert.False(ProjectManager.WorkerStarted);
} }
[ForegroundFact] [ForegroundFact]
@ -206,277 +434,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
// Assert // Assert
Assert.Empty(ProjectManager.Projects); Assert.Empty(ProjectManager.Projects);
Assert.True(ProjectManager.ListenersNotified); Assert.Equal(ProjectChangeKind.ProjectRemoved, ProjectManager.ListenersNotifiedOf);
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);
} }
[ForegroundFact] [ForegroundFact]
@ -490,8 +448,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
// Assert // Assert
Assert.Empty(ProjectManager.Projects); Assert.Empty(ProjectManager.Projects);
Assert.False(ProjectManager.ListenersNotified); Assert.Null(ProjectManager.ListenersNotifiedOf);
Assert.False(ProjectManager.WorkerStarted);
} }
[ForegroundFact] [ForegroundFact]
@ -508,8 +465,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
var snapshot = ProjectManager.GetSnapshot(WorkspaceProject); var snapshot = ProjectManager.GetSnapshot(WorkspaceProject);
Assert.False(snapshot.IsInitialized); Assert.False(snapshot.IsInitialized);
Assert.False(ProjectManager.ListenersNotified); Assert.Null(ProjectManager.ListenersNotifiedOf);
Assert.False(ProjectManager.WorkerStarted);
} }
[ForegroundFact] [ForegroundFact]
@ -527,8 +483,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
var snapshot = ProjectManager.GetSnapshot(WorkspaceProject); var snapshot = ProjectManager.GetSnapshot(WorkspaceProject);
Assert.Same(WorkspaceProject, snapshot.WorkspaceProject); Assert.Same(WorkspaceProject, snapshot.WorkspaceProject);
Assert.False(ProjectManager.ListenersNotified); Assert.Null(ProjectManager.ListenersNotifiedOf);
Assert.False(ProjectManager.WorkerStarted);
} }
[ForegroundFact] [ForegroundFact]
@ -545,12 +500,11 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
var snapshot = ProjectManager.GetSnapshot(WorkspaceProject); var snapshot = ProjectManager.GetSnapshot(WorkspaceProject);
Assert.False(snapshot.IsInitialized); Assert.False(snapshot.IsInitialized);
Assert.False(ProjectManager.ListenersNotified); Assert.Null(ProjectManager.ListenersNotifiedOf);
Assert.False(ProjectManager.WorkerStarted);
} }
[ForegroundFact] [ForegroundFact]
public void WorkspaceProjectAdded_WithHostProject_NotifiesListenters_AndStartsBackgroundWorker() public void WorkspaceProjectAdded_WithHostProject_NotifiesListenters()
{ {
// Arrange // Arrange
ProjectManager.HostProjectAdded(HostProject); ProjectManager.HostProjectAdded(HostProject);
@ -561,11 +515,45 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
// Assert // Assert
var snapshot = ProjectManager.GetSnapshot(WorkspaceProject); var snapshot = ProjectManager.GetSnapshot(WorkspaceProject);
Assert.True(snapshot.IsDirty);
Assert.True(snapshot.IsInitialized); Assert.True(snapshot.IsInitialized);
Assert.True(ProjectManager.ListenersNotified); Assert.Equal(ProjectChangeKind.ProjectChanged, ProjectManager.ListenersNotifiedOf);
Assert.True(ProjectManager.WorkerStarted); }
[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] [ForegroundFact]
@ -583,8 +571,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
// Assert // Assert
Assert.Empty(ProjectManager.Projects); Assert.Empty(ProjectManager.Projects);
Assert.False(ProjectManager.ListenersNotified); Assert.Null(ProjectManager.ListenersNotifiedOf);
Assert.False(ProjectManager.WorkerStarted);
} }
[ForegroundFact] [ForegroundFact]
@ -604,11 +591,9 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
var snapshot = ProjectManager.GetSnapshot(WorkspaceProject); var snapshot = ProjectManager.GetSnapshot(WorkspaceProject);
Assert.False(snapshot.IsInitialized); Assert.False(snapshot.IsInitialized);
Assert.False(ProjectManager.ListenersNotified); Assert.Null(ProjectManager.ListenersNotifiedOf);
Assert.False(ProjectManager.WorkerStarted);
} }
[ForegroundFact] [ForegroundFact]
public void WorkspaceProjectChanged_IgnoresProjectWithoutFilePath() public void WorkspaceProjectChanged_IgnoresProjectWithoutFilePath()
{ {
@ -626,8 +611,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
var snapshot = ProjectManager.GetSnapshot(WorkspaceProject); var snapshot = ProjectManager.GetSnapshot(WorkspaceProject);
Assert.False(snapshot.IsInitialized); Assert.False(snapshot.IsInitialized);
Assert.False(ProjectManager.ListenersNotified); Assert.Null(ProjectManager.ListenersNotifiedOf);
Assert.False(ProjectManager.WorkerStarted);
} }
[ForegroundFact] [ForegroundFact]
@ -645,65 +629,46 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
var snapshot = ProjectManager.GetSnapshot(WorkspaceProject); var snapshot = ProjectManager.GetSnapshot(WorkspaceProject);
Assert.Same(WorkspaceProject, snapshot.WorkspaceProject); Assert.Same(WorkspaceProject, snapshot.WorkspaceProject);
Assert.False(ProjectManager.ListenersNotified); Assert.Null(ProjectManager.ListenersNotifiedOf);
Assert.False(ProjectManager.WorkerStarted);
} }
[ForegroundFact] [ForegroundFact]
public void WorkspaceProjectChanged_MadeDirty_RetainsComputedState_NotifiesListeners_AndStartsBackgroundWorker() public async Task WorkspaceProjectRemoved_DoesNotRemoveProject_RemovesTagHelpers()
{ {
// Arrange // Arrange
ProjectManager.HostProjectAdded(HostProject); ProjectManager.HostProjectAdded(HostProject);
ProjectManager.WorkspaceProjectAdded(WorkspaceProject); ProjectManager.WorkspaceProjectAdded(WorkspaceProject);
ProjectManager.Reset(); ProjectManager.Reset();
// Generate the update
var snapshot = ProjectManager.GetSnapshot(HostProject); var snapshot = ProjectManager.GetSnapshot(HostProject);
var updateContext = snapshot.CreateUpdateContext();
ProjectManager.ProjectUpdated(updateContext);
ProjectManager.Reset();
var project = WorkspaceProject.WithAssemblyName("Test1"); // Simulate a project change // Adding some computed state
await snapshot.GetTagHelpersAsync();
// 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();
// Act // Act
ProjectManager.WorkspaceProjectRemoved(WorkspaceProject); ProjectManager.WorkspaceProjectRemoved(WorkspaceProject);
// Assert // Assert
var snapshot = ProjectManager.GetSnapshot(WorkspaceProject); snapshot = ProjectManager.GetSnapshot(WorkspaceProject);
Assert.True(snapshot.IsDirty);
Assert.False(snapshot.IsInitialized); Assert.False(snapshot.IsInitialized);
Assert.False(snapshot.TryGetTagHelpers(out var _));
Assert.True(ProjectManager.ListenersNotified); Assert.Equal(ProjectChangeKind.ProjectChanged, ProjectManager.ListenersNotifiedOf);
Assert.False(ProjectManager.WorkerStarted);
} }
[ForegroundFact] [ForegroundFact]
public void WorkspaceProjectRemoved_WithHostProject_FallsBackToSecondProject() public async Task WorkspaceProjectRemoved_FallsBackToSecondProject()
{ {
// Arrange // Arrange
ProjectManager.HostProjectAdded(HostProject); ProjectManager.HostProjectAdded(HostProject);
ProjectManager.WorkspaceProjectAdded(WorkspaceProject); ProjectManager.WorkspaceProjectAdded(WorkspaceProject);
ProjectManager.Reset(); 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 // Sets up a solution where the which has WorkspaceProjectWithDifferentTfm but not WorkspaceProject
// This will enable us to fall back and find the WorkspaceProjectWithDifferentTfm // This will enable us to fall back and find the WorkspaceProjectWithDifferentTfm
Assert.True(Workspace.TryApplyChanges(WorkspaceProjectWithDifferentTfm.Solution)); Assert.True(Workspace.TryApplyChanges(WorkspaceProjectWithDifferentTfm.Solution));
@ -712,13 +677,12 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
ProjectManager.WorkspaceProjectRemoved(WorkspaceProject); ProjectManager.WorkspaceProjectRemoved(WorkspaceProject);
// Assert // Assert
var snapshot = ProjectManager.GetSnapshot(WorkspaceProject); snapshot = ProjectManager.GetSnapshot(WorkspaceProject);
Assert.True(snapshot.IsDirty);
Assert.True(snapshot.IsInitialized); Assert.True(snapshot.IsInitialized);
Assert.Equal(WorkspaceProjectWithDifferentTfm.Id, snapshot.WorkspaceProject.Id); Assert.Equal(WorkspaceProjectWithDifferentTfm.Id, snapshot.WorkspaceProject.Id);
Assert.False(snapshot.TryGetTagHelpers(out var _));
Assert.True(ProjectManager.ListenersNotified); Assert.Equal(ProjectChangeKind.ProjectChanged, ProjectManager.ListenersNotifiedOf);
Assert.True(ProjectManager.WorkerStarted);
} }
[ForegroundFact] [ForegroundFact]
@ -736,8 +700,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
var snapshot = ProjectManager.GetSnapshot(WorkspaceProject); var snapshot = ProjectManager.GetSnapshot(WorkspaceProject);
Assert.Same(WorkspaceProject, snapshot.WorkspaceProject); Assert.Same(WorkspaceProject, snapshot.WorkspaceProject);
Assert.False(ProjectManager.ListenersNotified); Assert.Null(ProjectManager.ListenersNotifiedOf);
Assert.False(ProjectManager.WorkerStarted);
} }
[ForegroundFact] [ForegroundFact]
@ -755,8 +718,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
var snapshot = ProjectManager.GetSnapshot(WorkspaceProject); var snapshot = ProjectManager.GetSnapshot(WorkspaceProject);
Assert.False(snapshot.IsInitialized); Assert.False(snapshot.IsInitialized);
Assert.False(ProjectManager.ListenersNotified); Assert.Null(ProjectManager.ListenersNotifiedOf);
Assert.False(ProjectManager.WorkerStarted);
} }
[ForegroundFact] [ForegroundFact]
@ -774,8 +736,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
var snapshot = ProjectManager.GetSnapshot(WorkspaceProject); var snapshot = ProjectManager.GetSnapshot(WorkspaceProject);
Assert.False(snapshot.IsInitialized); Assert.False(snapshot.IsInitialized);
Assert.False(ProjectManager.ListenersNotified); Assert.Null(ProjectManager.ListenersNotifiedOf);
Assert.False(ProjectManager.WorkerStarted);
} }
[ForegroundFact] [ForegroundFact]
@ -789,20 +750,17 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
// Assert // Assert
Assert.Empty(ProjectManager.Projects); Assert.Empty(ProjectManager.Projects);
Assert.False(ProjectManager.ListenersNotified); Assert.Null(ProjectManager.ListenersNotifiedOf);
Assert.False(ProjectManager.WorkerStarted);
} }
private class TestProjectSnapshotManager : DefaultProjectSnapshotManager private class TestProjectSnapshotManager : DefaultProjectSnapshotManager
{ {
public TestProjectSnapshotManager(ForegroundDispatcher dispatcher, IEnumerable<ProjectSnapshotChangeTrigger> triggers, Workspace workspace) 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 ProjectChangeKind? ListenersNotifiedOf { get; private set; }
public bool WorkerStarted { get; private set; }
public DefaultProjectSnapshot GetSnapshot(HostProject hostProject) public DefaultProjectSnapshot GetSnapshot(HostProject hostProject)
{ {
@ -816,21 +774,12 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
public void Reset() public void Reset()
{ {
ListenersNotified = false; ListenersNotifiedOf = null;
WorkerStarted = false;
} }
protected override void NotifyListeners(ProjectChangeEventArgs e) protected override void NotifyListeners(ProjectChangeEventArgs e)
{ {
ListenersNotified = true; ListenersNotifiedOf = e.Kind;
}
protected override void NotifyBackgroundWorker(ProjectSnapshotUpdateContext context)
{
Assert.NotNull(context.HostProject);
Assert.NotNull(context.WorkspaceProject);
WorkerStarted = true;
} }
} }
} }

View File

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

View File

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

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. // Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information
using System.Collections.Generic;
using System.Collections.Immutable;
using Microsoft.VisualStudio.ProjectSystem.Properties; using Microsoft.VisualStudio.ProjectSystem.Properties;
namespace Microsoft.VisualStudio.ProjectSystem namespace Microsoft.VisualStudio.ProjectSystem
{ {
internal class TestProjectChangeDescription : IProjectChangeDescription 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; 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 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; } public IImmutableDictionary<NamedIdentity, IComparable> DataSourceVersions { get; }
} }

View File

@ -82,7 +82,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
{ {
return new ProjectVersionedValue<IProjectSubscriptionUpdate>( return new ProjectVersionedValue<IProjectSubscriptionUpdate>(
value: new ProjectSubscriptionUpdate( 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), projectConfiguration: ActiveConfiguredProject.ProjectConfiguration),
dataSourceVersions: ImmutableDictionary<NamedIdentity, IComparable>.Empty); dataSourceVersions: ImmutableDictionary<NamedIdentity, IComparable>.Empty);
} }

View File

@ -14,6 +14,41 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor
{ {
public class VsSolutionUpdatesProjectSnapshotChangeTriggerTest 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] [Fact]
public void Initialize_AttachesEventSink() public void Initialize_AttachesEventSink()
{ {
@ -38,10 +73,10 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor
} }
[Fact] [Fact]
public void UpdateProjectCfg_Done_KnownProject_Invokes_ProjectBuildComplete() public void UpdateProjectCfg_Done_KnownProject_Invokes_WorkspaceProjectChanged()
{ {
// Arrange // Arrange
var expectedProjectPath = "Path/To/Project"; var expectedProjectPath = SomeProject.FilePath;
uint cookie; uint cookie;
var buildManager = new Mock<IVsSolutionBuildManager>(MockBehavior.Strict); var buildManager = new Mock<IVsSolutionBuildManager>(MockBehavior.Strict);
@ -57,16 +92,19 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor
var projectSnapshots = new[] var projectSnapshots = new[]
{ {
Mock.Of<ProjectSnapshot>(p => p.FilePath == expectedProjectPath && p.HostProject == new HostProject(expectedProjectPath, RazorConfiguration.Default)), new DefaultProjectSnapshot(new ProjectState(Workspace.Services, SomeProject, SomeWorkspaceProject)),
Mock.Of<ProjectSnapshot>(p => p.FilePath == "Test2.csproj" && p.HostProject == new HostProject("Test2.csproj", RazorConfiguration.Default)), new DefaultProjectSnapshot(new ProjectState(Workspace.Services, SomeOtherProject, SomeOtherWorkspaceProject)),
}; };
var called = false; var called = false;
var projectManager = new Mock<ProjectSnapshotManagerBase>(); var projectManager = new Mock<ProjectSnapshotManagerBase>();
projectManager.SetupGet(p => p.Projects).Returns(projectSnapshots); projectManager.SetupGet(p => p.Workspace).Returns(Workspace);
projectManager projectManager
.Setup(p => p.HostProjectBuildComplete(It.IsAny<HostProject>())) .Setup(p => p.GetLoadedProject(expectedProjectPath))
.Callback<HostProject>(c => .Returns(projectSnapshots[0]);
projectManager
.Setup(p => p.WorkspaceProjectChanged(It.IsAny<Project>()))
.Callback<Project>(c =>
{ {
called = true; called = true;
Assert.Equal(expectedProjectPath, c.FilePath); Assert.Equal(expectedProjectPath, c.FilePath);
@ -83,7 +121,50 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor
} }
[Fact] [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 // Arrange
var expectedProjectPath = "Path/To/Project"; var expectedProjectPath = "Path/To/Project";
@ -102,15 +183,18 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor
var projectSnapshots = new[] 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, SomeWorkspaceProject)),
Mock.Of<ProjectSnapshot>(p => p.FilePath == "Path/To/DifferenProject" && p.HostProject == new HostProject("Path/To/DifferenProject", RazorConfiguration.Default)), new DefaultProjectSnapshot(new ProjectState(Workspace.Services, SomeOtherProject, SomeOtherWorkspaceProject)),
}; };
var projectManager = new Mock<ProjectSnapshotManagerBase>(); var projectManager = new Mock<ProjectSnapshotManagerBase>();
projectManager.SetupGet(p => p.Projects).Returns(projectSnapshots); projectManager.SetupGet(p => p.Workspace).Returns(Workspace);
projectManager projectManager
.Setup(p => p.HostProjectBuildComplete(It.IsAny<HostProject>())) .Setup(p => p.GetLoadedProject(expectedProjectPath))
.Callback<HostProject>(c => .Returns((ProjectSnapshot)null);
projectManager
.Setup(p => p.WorkspaceProjectChanged(It.IsAny<Project>()))
.Callback<Project>(c =>
{ {
throw new InvalidOperationException("This should not be called."); throw new InvalidOperationException("This should not be called.");
}); });

View File

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

View File

@ -10,29 +10,71 @@ using MonoDevelop.Projects;
using Moq; using Moq;
using Xunit; using Xunit;
using Project = Microsoft.CodeAnalysis.Project; using Project = Microsoft.CodeAnalysis.Project;
using Workspace = Microsoft.CodeAnalysis.Workspace;
namespace Microsoft.VisualStudio.Mac.LanguageServices.Razor namespace Microsoft.VisualStudio.Mac.LanguageServices.Razor
{ {
public class ProjectBuildChangeTriggerTest : ForegroundDispatcherTestBase 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] [ForegroundFact]
public void ProjectOperations_EndBuild_Invokes_ProjectBuildComplete() public void ProjectOperations_EndBuild_Invokes_WorkspaceProjectChanged()
{ {
// Arrange // Arrange
var args = new BuildEventArgs(monitor: null, success: true); var expectedProjectPath = SomeProject.FilePath;
var expectedProjectPath = "Path/To/Project";
var projectService = CreateProjectService(expectedProjectPath); var projectService = CreateProjectService(expectedProjectPath);
var args = new BuildEventArgs(monitor: null, success: true);
var projectSnapshots = new[] var projectSnapshots = new[]
{ {
Mock.Of<ProjectSnapshot>(p => p.FilePath == expectedProjectPath && p.HostProject == new HostProject(expectedProjectPath, RazorConfiguration.Default)), new DefaultProjectSnapshot(new ProjectState(Workspace.Services, SomeProject, SomeWorkspaceProject)),
Mock.Of<ProjectSnapshot>(p => p.FilePath == "Test2.csproj" && p.HostProject == new HostProject("Test2.csproj", RazorConfiguration.Default)), new DefaultProjectSnapshot(new ProjectState(Workspace.Services, SomeOtherProject, SomeOtherWorkspaceProject)),
}; };
var projectManager = new Mock<ProjectSnapshotManagerBase>(MockBehavior.Strict); var projectManager = new Mock<ProjectSnapshotManagerBase>(MockBehavior.Strict);
projectManager.SetupGet(p => p.Projects).Returns(projectSnapshots); projectManager.SetupGet(p => p.Workspace).Returns(Workspace);
projectManager projectManager
.Setup(p => p.HostProjectBuildComplete(It.IsAny<HostProject>())) .Setup(p => p.GetLoadedProject(SomeProject.FilePath))
.Callback<HostProject>(c => Assert.Equal(expectedProjectPath, c.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); var trigger = new ProjectBuildChangeTrigger(Dispatcher, projectService, projectManager.Object);
// Act // Act
@ -42,21 +84,56 @@ namespace Microsoft.VisualStudio.Mac.LanguageServices.Razor
projectManager.VerifyAll(); 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] [ForegroundFact]
public void ProjectOperations_EndBuild_UntrackedProject_Noops() public void ProjectOperations_EndBuild_UntrackedProject_Noops()
{ {
// Arrange // Arrange
var args = new BuildEventArgs(monitor: null, success: true);
var projectService = CreateProjectService("Path/To/Project"); var projectService = CreateProjectService("Path/To/Project");
var args = new BuildEventArgs(monitor: null, success: true);
var projectSnapshots = new[] 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>(); var projectManager = new Mock<ProjectSnapshotManagerBase>();
projectManager.SetupGet(p => p.Projects).Returns(projectSnapshots); projectManager.SetupGet(p => p.Workspace).Returns(Workspace);
projectManager 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>(); .Throws<InvalidOperationException>();
var trigger = new ProjectBuildChangeTrigger(Dispatcher, projectService, projectManager.Object); var trigger = new ProjectBuildChangeTrigger(Dispatcher, projectService, projectManager.Object);
// Act & Assert // Act & Assert
@ -100,11 +177,5 @@ namespace Microsoft.VisualStudio.Mac.LanguageServices.Razor
projectService.Setup(p => p.IsSupportedProject(null)).Returns(true); projectService.Setup(p => p.IsSupportedProject(null)).Returns(true);
return projectService.Object; 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"> <Compile Include="DocumentInfo\RazorDocumentInfoWindowControl.xaml.cs">
<DependentUpon>RazorDocumentInfoWindowControl.xaml</DependentUpon> <DependentUpon>RazorDocumentInfoWindowControl.xaml</DependentUpon>
</Compile> </Compile>
<Compile Include="RazorInfo\AssemblyViewModel.cs" />
<Compile Include="Behaviors\ItemSelectedBehavior.cs" /> <Compile Include="Behaviors\ItemSelectedBehavior.cs" />
<Compile Include="Properties\AssemblyInfo.cs" /> <Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Properties\BindingRedirectAttributes.cs" /> <Compile Include="Properties\BindingRedirectAttributes.cs" />
<Compile Include="RazorInfo\DirectiveViewModel.cs" /> <Compile Include="RazorInfo\DirectiveDescriptorViewModel.cs" />
<Compile Include="RazorInfo\DocumentInfoViewModel.cs" /> <Compile Include="RazorInfo\DocumentSnapshotViewModel.cs" />
<Compile Include="RazorInfo\DocumentViewModel.cs" />
<Compile Include="NotifyPropertyChanged.cs" /> <Compile Include="NotifyPropertyChanged.cs" />
<Compile Include="RazorInfo\ProjectSnapshotViewModel.cs" /> <Compile Include="RazorInfo\ProjectSnapshotViewModel.cs" />
<Compile Include="RazorInfo\ProjectViewModel.cs" /> <Compile Include="RazorInfo\ProjectViewModel.cs" />

View File

@ -7,11 +7,11 @@ using Microsoft.AspNetCore.Razor.Language;
namespace Microsoft.VisualStudio.RazorExtension.RazorInfo namespace Microsoft.VisualStudio.RazorExtension.RazorInfo
{ {
public class DirectiveViewModel : NotifyPropertyChanged public class DirectiveDescriptorViewModel : NotifyPropertyChanged
{ {
private readonly DirectiveDescriptor _directive; private readonly DirectiveDescriptor _directive;
internal DirectiveViewModel(DirectiveDescriptor directive) internal DirectiveDescriptorViewModel(DirectiveDescriptor directive)
{ {
_directive = 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
#if RAZOR_EXTENSION_DEVELOPER_MODE #if RAZOR_EXTENSION_DEVELOPER_MODE
using Microsoft.CodeAnalysis.Razor.ProjectSystem; using Microsoft.CodeAnalysis.Razor.ProjectSystem;
namespace Microsoft.VisualStudio.RazorExtension.RazorInfo namespace Microsoft.VisualStudio.RazorExtension.RazorInfo
{ {
public class AssemblyViewModel : NotifyPropertyChanged public class DocumentSnapshotViewModel : NotifyPropertyChanged
{ {
private readonly ProjectExtensibilityAssembly _assembly; internal DocumentSnapshotViewModel(DocumentSnapshot document)
internal AssemblyViewModel(ProjectExtensibilityAssembly assembly)
{ {
_assembly = assembly; Document = document;
Name = _assembly.Identity.GetDisplayName();
} }
public string Name { get; } internal DocumentSnapshot Document { get; }
public string FilePath => Document.FilePath;
public string TargetPath => Document.TargetPath;
} }
} }
#endif #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 #if RAZOR_EXTENSION_DEVELOPER_MODE
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Windows;
namespace Microsoft.VisualStudio.RazorExtension.RazorInfo namespace Microsoft.VisualStudio.RazorExtension.RazorInfo
{ {
public class ProjectInfoViewModel : NotifyPropertyChanged public class ProjectInfoViewModel : NotifyPropertyChanged
{ {
private ObservableCollection<DirectiveViewModel> _directives; private ObservableCollection<DirectiveDescriptorViewModel> _directives;
private ObservableCollection<DocumentViewModel> _documents; private ObservableCollection<DocumentSnapshotViewModel> _documents;
private ObservableCollection<TagHelperViewModel> _tagHelpers; private ObservableCollection<TagHelperViewModel> _tagHelpers;
private bool _tagHelpersLoading;
public ObservableCollection<DirectiveViewModel> Directives public ObservableCollection<DirectiveDescriptorViewModel> Directives
{ {
get { return _directives; } get { return _directives; }
set set
@ -22,7 +24,7 @@ namespace Microsoft.VisualStudio.RazorExtension.RazorInfo
} }
} }
public ObservableCollection<DocumentViewModel> Documents public ObservableCollection<DocumentSnapshotViewModel> Documents
{ {
get { return _documents; } get { return _documents; }
set set
@ -41,6 +43,20 @@ namespace Microsoft.VisualStudio.RazorExtension.RazorInfo
OnPropertyChanged(); OnPropertyChanged();
} }
} }
public bool TagHelpersLoading
{
get { return _tagHelpersLoading; }
set
{
_tagHelpersLoading = value;
OnPropertyChanged();
OnPropertyChanged(nameof(TagHelperProgressVisibility));
}
}
public Visibility TagHelperProgressVisibility => TagHelpersLoading ? Visibility.Visible : Visibility.Hidden;
} }
} }
#endif #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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
#if RAZOR_EXTENSION_DEVELOPER_MODE #if RAZOR_EXTENSION_DEVELOPER_MODE
using System;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.IO; using System.IO;
using System.Linq;
using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Razor.ProjectSystem; using Microsoft.CodeAnalysis.Razor.ProjectSystem;
@ -16,12 +18,9 @@ namespace Microsoft.VisualStudio.RazorExtension.RazorInfo
Project = project; Project = project;
Id = project.WorkspaceProject?.Id; Id = project.WorkspaceProject?.Id;
Properties = new ObservableCollection<PropertyViewModel>() Properties = new ObservableCollection<PropertyViewModel>();
{
new PropertyViewModel("Razor Language Version", project.Configuration?.LanguageVersion.ToString()), InitializeProperties();
new PropertyViewModel("Configuration Name", $"{project.Configuration?.ConfigurationName} ({project.Configuration?.GetType().Name ?? "unknown"})"),
new PropertyViewModel("Workspace Project", project.WorkspaceProject?.Name)
};
} }
internal ProjectSnapshot Project { get; } internal ProjectSnapshot Project { get; }
@ -31,6 +30,26 @@ namespace Microsoft.VisualStudio.RazorExtension.RazorInfo
public ProjectId Id { get; } public ProjectId Id { get; }
public ObservableCollection<PropertyViewModel> Properties { 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 #endif

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