diff --git a/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Performance/ProjectSystem/BackgroundCodeGenerationBenchmark.cs b/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Performance/ProjectSystem/BackgroundCodeGenerationBenchmark.cs index 40b903844b0..db9c09aa4a3 100644 --- a/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Performance/ProjectSystem/BackgroundCodeGenerationBenchmark.cs +++ b/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Performance/ProjectSystem/BackgroundCodeGenerationBenchmark.cs @@ -14,7 +14,7 @@ public class BackgroundCodeGenerationBenchmark : ProjectSnapshotManagerBenchmark public void Setup() { SnapshotManager = CreateProjectSnapshotManager(); - SnapshotManager.HostProjectAdded(HostProject); + SnapshotManager.ProjectAdded(HostProject); SnapshotManager.Changed += SnapshotManager_Changed; } diff --git a/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Performance/ProjectSystem/ProjectLoadBenchmark.cs b/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Performance/ProjectSystem/ProjectLoadBenchmark.cs index 58baba8faf8..73403ed515f 100644 --- a/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Performance/ProjectSystem/ProjectLoadBenchmark.cs +++ b/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Performance/ProjectSystem/ProjectLoadBenchmark.cs @@ -19,7 +19,7 @@ public void Setup() [Benchmark(Description = "Initializes a project and 100 files", OperationsPerInvoke = 100)] public void ProjectLoad_AddProjectAnd100Files() { - SnapshotManager.HostProjectAdded(HostProject); + SnapshotManager.ProjectAdded(HostProject); for (var i= 0; i < Documents.Length; i++) { diff --git a/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Performance/ProjectSystem/ProjectSnapshotManagerBenchmarkBase.cs b/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Performance/ProjectSystem/ProjectSnapshotManagerBenchmarkBase.cs index aab2fc4527a..cd4251769cf 100644 --- a/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Performance/ProjectSystem/ProjectSnapshotManagerBenchmarkBase.cs +++ b/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Performance/ProjectSystem/ProjectSnapshotManagerBenchmarkBase.cs @@ -124,7 +124,7 @@ public StaticTagHelperResolver(IReadOnlyList tagHelpers) this._tagHelpers = tagHelpers; } - public override Task GetTagHelpersAsync(ProjectSnapshot project, CancellationToken cancellationToken = default) + public override Task GetTagHelpersAsync(Project project, ProjectSnapshot projectSnapshot, CancellationToken cancellationToken = default) { return Task.FromResult(new TagHelperResolutionResult(_tagHelpers, Array.Empty())); } diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/DefaultProjectWorkspaceStateGenerator.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/DefaultProjectWorkspaceStateGenerator.cs new file mode 100644 index 00000000000..0cd08df4f68 --- /dev/null +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/DefaultProjectWorkspaceStateGenerator.cs @@ -0,0 +1,214 @@ +// 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.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Razor.ProjectSystem; + +namespace Microsoft.CodeAnalysis.Razor +{ + [Shared] + [Export(typeof(ProjectWorkspaceStateGenerator))] + [Export(typeof(ProjectSnapshotChangeTrigger))] + internal class DefaultProjectWorkspaceStateGenerator : ProjectWorkspaceStateGenerator, IDisposable + { + // Internal for testing + internal readonly Dictionary _updates; + + private readonly ForegroundDispatcher _foregroundDispatcher; + private ProjectSnapshotManagerBase _projectManager; + private TagHelperResolver _tagHelperResolver; + + [ImportingConstructor] + public DefaultProjectWorkspaceStateGenerator(ForegroundDispatcher foregroundDispatcher) + { + if (foregroundDispatcher == null) + { + throw new ArgumentNullException(nameof(foregroundDispatcher)); + } + + _foregroundDispatcher = foregroundDispatcher; + + _updates = new Dictionary(FilePathComparer.Instance); + } + + // 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 NotifyBackgroundWorkCompleted { get; set; } + + public override void Initialize(ProjectSnapshotManagerBase projectManager) + { + if (projectManager == null) + { + throw new ArgumentNullException(nameof(projectManager)); + } + + _projectManager = projectManager; + + var razorLanguageServices = _projectManager.Workspace.Services.GetLanguageServices(RazorLanguage.Name); + _tagHelperResolver = razorLanguageServices.GetRequiredService(); + } + + public override void Update(Project workspaceProject, ProjectSnapshot projectSnapshot) + { + if (projectSnapshot == null) + { + throw new ArgumentNullException(nameof(projectSnapshot)); + } + + _foregroundDispatcher.AssertForegroundThread(); + + if (_updates.TryGetValue(projectSnapshot.FilePath, out var updateItem) && + !updateItem.Task.IsCompleted) + { + updateItem.Cts.Cancel(); + } + + updateItem?.Cts.Dispose(); + + var cts = new CancellationTokenSource(); + var updateTask = Task.Factory.StartNew( + () => UpdateWorkspaceStateAsync(workspaceProject, projectSnapshot, cts.Token), + cts.Token, + TaskCreationOptions.None, + _foregroundDispatcher.BackgroundScheduler).Unwrap(); + updateTask.ConfigureAwait(false); + updateItem = new UpdateItem(updateTask, cts); + _updates[projectSnapshot.FilePath] = updateItem; + } + + public void Dispose() + { + _foregroundDispatcher.AssertForegroundThread(); + + foreach (var update in _updates) + { + if (!update.Value.Task.IsCompleted) + { + update.Value.Cts.Cancel(); + } + } + + BlockBackgroundWorkStart?.Set(); + } + + private async Task UpdateWorkspaceStateAsync(Project workspaceProject, ProjectSnapshot projectSnapshot, CancellationToken cancellationToken) + { + try + { + _foregroundDispatcher.AssertBackgroundThread(); + + OnStartingBackgroundWork(); + + if (cancellationToken.IsCancellationRequested) + { + // Silently cancel, we're the only ones creating these tasks. + return; + } + + var workspaceState = ProjectWorkspaceState.Default; + try + { + if (workspaceProject != null) + { + var tagHelperResolutionResult = await _tagHelperResolver.GetTagHelpersAsync(workspaceProject, projectSnapshot, cancellationToken); + workspaceState = new ProjectWorkspaceState(tagHelperResolutionResult.Descriptors); + } + } + catch (Exception ex) + { + await Task.Factory.StartNew( + () => _projectManager.ReportError(ex, projectSnapshot), + CancellationToken.None, // Don't allow errors to be cancelled + TaskCreationOptions.None, + _foregroundDispatcher.ForegroundScheduler).ConfigureAwait(false); + return; + } + + if (cancellationToken.IsCancellationRequested) + { + // Silently cancel, we're the only ones creating these tasks. + return; + } + + await Task.Factory.StartNew( + () => + { + if (cancellationToken.IsCancellationRequested) + { + return; + } + + ReportWorkspaceStateChange(projectSnapshot.FilePath, workspaceState); + }, + cancellationToken, + TaskCreationOptions.None, + _foregroundDispatcher.ForegroundScheduler).ConfigureAwait(false); + } + catch (Exception ex) + { + // This is something totally unexpected, let's just send it over to the project manager. + await Task.Factory.StartNew( + () => _projectManager.ReportError(ex), + CancellationToken.None, // Don't allow errors to be cancelled + TaskCreationOptions.None, + _foregroundDispatcher.ForegroundScheduler).ConfigureAwait(false); + } + + OnBackgroundWorkCompleted(); + } + + private void ReportWorkspaceStateChange(string projectFilePath, ProjectWorkspaceState workspaceStateChange) + { + _foregroundDispatcher.AssertForegroundThread(); + + _projectManager.ProjectWorkspaceStateChanged(projectFilePath, workspaceStateChange); + } + + private void OnStartingBackgroundWork() + { + if (BlockBackgroundWorkStart != null) + { + BlockBackgroundWorkStart.Wait(); + BlockBackgroundWorkStart.Reset(); + } + } + + private void OnBackgroundWorkCompleted() + { + if (NotifyBackgroundWorkCompleted != null) + { + NotifyBackgroundWorkCompleted.Set(); + } + } + + // Internal for testing + internal class UpdateItem + { + public UpdateItem(Task task, CancellationTokenSource cts) + { + if (task == null) + { + throw new ArgumentNullException(nameof(task)); + } + + if (cts == null) + { + throw new ArgumentNullException(nameof(cts)); + } + + Task = task; + Cts = cts; + } + + public Task Task { get; } + + public CancellationTokenSource Cts { get; } + } + } +} \ No newline at end of file diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultProjectSnapshot.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultProjectSnapshot.cs index dff471af3dd..87fd74b8b19 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultProjectSnapshot.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultProjectSnapshot.cs @@ -38,11 +38,15 @@ public DefaultProjectSnapshot(ProjectState state) public HostProject HostProject => State.HostProject; - public override bool IsInitialized => WorkspaceProject != null; - public override VersionStamp Version => State.Version; - public override Project WorkspaceProject => State.WorkspaceProject; + public override IReadOnlyList TagHelpers => State.TagHelpers; + +#pragma warning disable CS0672 // Member overrides obsolete member + public override bool IsInitialized => throw new NotImplementedException(); + + public override Project WorkspaceProject => throw new NotImplementedException(); +#pragma warning restore CS0672 // Member overrides obsolete member public override DocumentSnapshot GetDocument(string filePath) { @@ -92,22 +96,17 @@ public override RazorProjectEngine GetProjectEngine() return State.ProjectEngine; } +#pragma warning disable CS0672 // Member overrides obsolete member public override Task> GetTagHelpersAsync() { - // IMPORTANT: Don't put more code here. We want this to return a cached task. - return State.GetTagHelpersAsync(this); + return Task.FromResult(TagHelpers); } public override bool TryGetTagHelpers(out IReadOnlyList result) { - if (State.IsTagHelperResultAvailable) - { - result = State.GetTagHelpersAsync(this).Result; - return true; - } - - result = null; - return false; + result = TagHelpers; + return true; } +#pragma warning restore CS0672 // Member overrides obsolete member } } \ No newline at end of file diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultProjectSnapshotManager.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultProjectSnapshotManager.cs index 9953878518b..191ce4b1238 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultProjectSnapshotManager.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultProjectSnapshotManager.cs @@ -10,22 +10,13 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem { - // The implementation of project snapshot manager abstracts over the Roslyn Project (WorkspaceProject) - // and information from the host's underlying project system (HostProject), to provide a unified and - // immutable view of the underlying project systems. + // The implementation of project snapshot manager abstracts the host's underlying project system (HostProject), + // to provide a immutable view of the underlying project systems. // // The HostProject support all of the configuration that the Razor SDK exposes via the project system // (language version, extensions, named configuration). // - // The WorkspaceProject is needed to support our use of Roslyn Compilations for Tag Helpers and other - // C# based constructs. - // - // The implementation will create a ProjectSnapshot for each HostProject. Put another way, when we - // see a WorkspaceProject get created, we only care if we already have a HostProject for the same - // filepath. - // - // Our underlying HostProject infrastructure currently does not handle multiple TFMs (project with - // $(TargetFrameworks), so we just bind to the first WorkspaceProject we see for each HostProject. + // The implementation will create a ProjectSnapshot for each HostProject. internal class DefaultProjectSnapshotManager : ProjectSnapshotManagerBase { public override event EventHandler Changed; @@ -385,7 +376,7 @@ public override void DocumentChanged(string projectFilePath, string documentFile } } - public override void HostProjectAdded(HostProject hostProject) + public override void ProjectAdded(HostProject hostProject) { if (hostProject == null) { @@ -400,11 +391,7 @@ public override void HostProjectAdded(HostProject hostProject) return; } - // It's possible that Workspace has already created a project for this, but it's not deterministic - // So if possible find a WorkspaceProject. - var workspaceProject = GetWorkspaceProject(hostProject.FilePath); - - var state = ProjectState.Create(Workspace.Services, hostProject, workspaceProject); + var state = ProjectState.Create(Workspace.Services, hostProject); var entry = new Entry(state); _projects[hostProject.FilePath] = entry; @@ -412,7 +399,7 @@ public override void HostProjectAdded(HostProject hostProject) NotifyListeners(new ProjectChangeEventArgs(null, entry.GetSnapshot(), ProjectChangeKind.ProjectAdded)); } - public override void HostProjectChanged(HostProject hostProject) + public override void ProjectConfigurationChanged(HostProject hostProject) { if (hostProject == null) { @@ -436,138 +423,50 @@ public override void HostProjectChanged(HostProject hostProject) } } - public override void HostProjectRemoved(HostProject hostProject) - { - if (hostProject == null) - { - throw new ArgumentNullException(nameof(hostProject)); - } - - _foregroundDispatcher.AssertForegroundThread(); - - if (_projects.TryGetValue(hostProject.FilePath, out var entry)) - { - // We need to notify listeners about every project removal. - var oldSnapshot = entry.GetSnapshot(); - _projects.Remove(hostProject.FilePath); - NotifyListeners(new ProjectChangeEventArgs(oldSnapshot, null, ProjectChangeKind.ProjectRemoved)); - } - } - - public override void WorkspaceProjectAdded(Project workspaceProject) + public override void ProjectWorkspaceStateChanged(string projectFilePath, ProjectWorkspaceState projectWorkspaceState) { - if (workspaceProject == null) - { - throw new ArgumentNullException(nameof(workspaceProject)); - } - - _foregroundDispatcher.AssertForegroundThread(); - - if (!IsSupportedWorkspaceProject(workspaceProject)) - { - return; - } - - // The WorkspaceProject initialization never triggers a "Project Add" from out point of view, we - // only care if the new WorkspaceProject matches an existing HostProject. - if (_projects.TryGetValue(workspaceProject.FilePath, out var entry)) + if (projectFilePath == null) { - // If this is a multi-targeting project then we are only interested in a single workspace project. If we already - // found one in the past just ignore this one. - if (entry.State.WorkspaceProject == null) - { - var state = entry.State.WithWorkspaceProject(workspaceProject); - - var oldSnapshot = entry.GetSnapshot(); - entry = new Entry(state); - _projects[workspaceProject.FilePath] = entry; - NotifyListeners(new ProjectChangeEventArgs(oldSnapshot, entry.GetSnapshot(), ProjectChangeKind.ProjectChanged)); - } + throw new ArgumentNullException(nameof(projectFilePath)); } - } - public override void WorkspaceProjectChanged(Project workspaceProject) - { - if (workspaceProject == null) + if (projectWorkspaceState == null) { - throw new ArgumentNullException(nameof(workspaceProject)); + throw new ArgumentNullException(nameof(projectWorkspaceState)); } _foregroundDispatcher.AssertForegroundThread(); - if (!IsSupportedWorkspaceProject(workspaceProject)) - { - return; - } - - // We also need to check the projectId here. If this is a multi-targeting project then we are only interested - // in a single workspace project. Just use the one that showed up first. - if (_projects.TryGetValue(workspaceProject.FilePath, out var entry) && - (entry.State.WorkspaceProject == null || entry.State.WorkspaceProject.Id == workspaceProject.Id) && - (entry.State.WorkspaceProject == null || entry.State.WorkspaceProject.Version.GetNewerVersion(workspaceProject.Version) == workspaceProject.Version)) + if (_projects.TryGetValue(projectFilePath, out var entry)) { - var state = entry.State.WithWorkspaceProject(workspaceProject); + var state = entry.State.WithProjectWorkspaceState(projectWorkspaceState); - // WorkspaceProject updates can no-op. This can be the case if a build is triggered, but we've - // already seen the update. + // HostProject updates can no-op. if (!object.ReferenceEquals(state, entry.State)) { var oldSnapshot = entry.GetSnapshot(); entry = new Entry(state); - _projects[workspaceProject.FilePath] = entry; + _projects[projectFilePath] = entry; NotifyListeners(new ProjectChangeEventArgs(oldSnapshot, entry.GetSnapshot(), ProjectChangeKind.ProjectChanged)); } } } - public override void WorkspaceProjectRemoved(Project workspaceProject) + public override void ProjectRemoved(HostProject hostProject) { - if (workspaceProject == null) + if (hostProject == null) { - throw new ArgumentNullException(nameof(workspaceProject)); + throw new ArgumentNullException(nameof(hostProject)); } _foregroundDispatcher.AssertForegroundThread(); - if (!IsSupportedWorkspaceProject(workspaceProject)) - { - return; - } - - if (_projects.TryGetValue(workspaceProject.FilePath, out var entry)) + if (_projects.TryGetValue(hostProject.FilePath, out var entry)) { - // We also need to check the projectId here. If this is a multi-targeting project then we are only interested - // in a single workspace project. Make sure the WorkspaceProject we're using is the one that's being removed. - if (entry.State.WorkspaceProject?.Id != workspaceProject.Id) - { - return; - } - - ProjectState state; - - // So if the WorkspaceProject got removed, we should double check to make sure that there aren't others - // hanging around. This could happen if a project is multi-targeting and one of the TFMs is removed. - var otherWorkspaceProject = GetWorkspaceProject(workspaceProject.FilePath); - if (otherWorkspaceProject != null && otherWorkspaceProject.Id != workspaceProject.Id) - { - // OK there's another WorkspaceProject, use that. - state = entry.State.WithWorkspaceProject(otherWorkspaceProject); - - var oldSnapshot = entry.GetSnapshot(); - entry = new Entry(state); - _projects[otherWorkspaceProject.FilePath] = entry; - NotifyListeners(new ProjectChangeEventArgs(oldSnapshot, entry.GetSnapshot(), ProjectChangeKind.ProjectChanged)); - } - else - { - // Notify listeners of a change because we've removed computed state. - state = entry.State.WithWorkspaceProject(null); - - var oldSnapshot = entry.GetSnapshot(); - entry = new Entry(state); - _projects[workspaceProject.FilePath] = entry; - NotifyListeners(new ProjectChangeEventArgs(oldSnapshot, entry.GetSnapshot(), ProjectChangeKind.ProjectChanged)); - } + // We need to notify listeners about every project removal. + var oldSnapshot = entry.GetSnapshot(); + _projects.Remove(hostProject.FilePath); + NotifyListeners(new ProjectChangeEventArgs(oldSnapshot, null, ProjectChangeKind.ProjectRemoved)); } } @@ -602,41 +501,6 @@ public override void ReportError(Exception exception, HostProject hostProject) _errorReporter.ReportError(exception, snapshot); } - public override void ReportError(Exception exception, Project workspaceProject) - { - if (exception == null) - { - throw new ArgumentNullException(nameof(exception)); - } - - _errorReporter.ReportError(exception, workspaceProject); - } - - // We're only interested in CSharp projects that have a FilePath. We rely on the FilePath to - // unify the Workspace Project with our HostProject concept. - private bool IsSupportedWorkspaceProject(Project workspaceProject) => workspaceProject.Language == LanguageNames.CSharp && workspaceProject.FilePath != null; - - private Project GetWorkspaceProject(string filePath) - { - var solution = Workspace.CurrentSolution; - if (solution == null) - { - return null; - } - - foreach (var workspaceProject in solution.Projects) - { - if (IsSupportedWorkspaceProject(workspaceProject) && - FilePathComparer.Instance.Equals(filePath, workspaceProject.FilePath)) - { - // We don't try to handle mulitple TFMs anwhere in Razor, just take the first WorkspaceProject that is a match. - return workspaceProject; - } - } - - return null; - } - // virtual so it can be overridden in tests protected virtual void NotifyListeners(ProjectChangeEventArgs e) { diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DocumentState.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DocumentState.cs index 7c18ef06312..b6fe268e641 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DocumentState.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DocumentState.cs @@ -195,7 +195,7 @@ public virtual DocumentState WithImportsChange() return state; } - public virtual DocumentState WithWorkspaceProjectChange() + public virtual DocumentState WithProjectWorkspaceStateChange() { var state = new DocumentState(Services, HostDocument, _sourceText, _version, _loader); @@ -323,7 +323,8 @@ public ComputedStateTracker(DocumentState state, ComputedStateTracker older = nu // - This document // // All of these things are cached, so no work is wasted if we do need to generate the code. - var computedStateVersion = await project.State.GetComputedStateVersionAsync(project).ConfigureAwait(false); + var configurationVersion = project.State.ConfigurationVersion; + var projectWorkspaceStateVersion = project.State.ProjectWorkspaceStateVersion; var documentCollectionVersion = project.State.DocumentCollectionVersion; var imports = await GetImportsAsync(project, document).ConfigureAwait(false); var documentVersion = await document.GetTextVersionAsync().ConfigureAwait(false); @@ -331,9 +332,14 @@ public ComputedStateTracker(DocumentState state, ComputedStateTracker older = nu // OK now that have the previous output and all of the versions, we can see if anything // has changed that would require regenerating the code. var inputVersion = documentVersion; - if (inputVersion.GetNewerVersion(computedStateVersion) == computedStateVersion) + if (inputVersion.GetNewerVersion(configurationVersion) == configurationVersion) { - inputVersion = computedStateVersion; + inputVersion = configurationVersion; + } + + if (inputVersion.GetNewerVersion(projectWorkspaceStateVersion) == projectWorkspaceStateVersion) + { + inputVersion = projectWorkspaceStateVersion; } if (inputVersion.GetNewerVersion(documentCollectionVersion) == documentCollectionVersion) @@ -369,7 +375,6 @@ public ComputedStateTracker(DocumentState state, ComputedStateTracker older = nu } // OK we have to generate the code. - var tagHelpers = await project.GetTagHelpersAsync().ConfigureAwait(false); var importSources = new List(); foreach (var item in imports) { @@ -381,7 +386,7 @@ public ComputedStateTracker(DocumentState state, ComputedStateTracker older = nu var projectEngine = project.GetProjectEngine(); - var codeDocument = projectEngine.ProcessDesignTime(documentSource, fileKind: document.FileKind, importSources, tagHelpers); + var codeDocument = projectEngine.ProcessDesignTime(documentSource, fileKind: document.FileKind, importSources, project.TagHelpers); var csharpDocument = codeDocument.GetCSharpDocument(); // OK now we've generated the code. Let's check if the output is actually different. This is diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/EphemeralProjectSnapshot.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/EphemeralProjectSnapshot.cs index 82f9a93196b..f0520bade55 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/EphemeralProjectSnapshot.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/EphemeralProjectSnapshot.cs @@ -11,8 +11,6 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem { internal class EphemeralProjectSnapshot : ProjectSnapshot { - private static readonly Task> EmptyTagHelpers = Task.FromResult>(Array.Empty()); - private readonly HostWorkspaceServices _services; private readonly Lazy _projectEngine; @@ -40,11 +38,15 @@ public EphemeralProjectSnapshot(HostWorkspaceServices services, string filePath) public override string FilePath { get; } - public override bool IsInitialized => false; - public override VersionStamp Version { get; } = VersionStamp.Default; + public override IReadOnlyList TagHelpers { get; } = Array.Empty(); + +#pragma warning disable CS0672 // Member overrides obsolete member + public override bool IsInitialized => false; + public override Project WorkspaceProject => null; +#pragma warning restore CS0672 // Member overrides obsolete member public override DocumentSnapshot GetDocument(string filePath) { @@ -81,16 +83,18 @@ public override RazorProjectEngine GetProjectEngine() return _projectEngine.Value; } +#pragma warning disable CS0672 // Member overrides obsolete member public override Task> GetTagHelpersAsync() { - return EmptyTagHelpers; + return Task.FromResult(TagHelpers); } public override bool TryGetTagHelpers(out IReadOnlyList result) { - result = EmptyTagHelpers.Result; + result = TagHelpers; return true; } +#pragma warning restore CS0672 // Member overrides obsolete member private RazorProjectEngine CreateProjectEngine() { diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/LiveShareProjectSnapshotBase.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/LiveShareProjectSnapshotBase.cs index ae13f49735b..d1f7e2c3b69 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/LiveShareProjectSnapshotBase.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/LiveShareProjectSnapshotBase.cs @@ -19,12 +19,8 @@ internal class LiveShareProjectSnapshotBase : ProjectSnapshot public override string FilePath => throw new NotImplementedException(); - public override bool IsInitialized => throw new NotImplementedException(); - public override VersionStamp Version => throw new NotImplementedException(); - public override Project WorkspaceProject => throw new NotImplementedException(); - public override DocumentSnapshot GetDocument(string filePath) => throw new NotImplementedException(); public override bool IsImportDocument(DocumentSnapshot document) => throw new NotImplementedException(); @@ -33,8 +29,14 @@ internal class LiveShareProjectSnapshotBase : ProjectSnapshot public override RazorProjectEngine GetProjectEngine() => throw new NotImplementedException(); +#pragma warning disable CS0672 // Member overrides obsolete member + public override bool IsInitialized => throw new NotImplementedException(); + + public override Project WorkspaceProject => throw new NotImplementedException(); + public override Task> GetTagHelpersAsync() => throw new NotImplementedException(); public override bool TryGetTagHelpers(out IReadOnlyList result) => throw new NotImplementedException(); +#pragma warning restore CS0672 // Member overrides obsolete member } } diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectDifference.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectDifference.cs index 138acdb312d..6822aa92a7e 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectDifference.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectDifference.cs @@ -10,11 +10,9 @@ internal enum ProjectDifference { None = 0, ConfigurationChanged = 1, - WorkspaceProjectAdded = 2, - WorkspaceProjectRemoved = 4, - WorkspaceProjectChanged = 8, - DocumentAdded = 16, - DocumentRemoved = 32, - DocumentChanged = 64, + ProjectWorkspaceStateChanged = 2, + DocumentAdded = 4, + DocumentRemoved = 8, + DocumentChanged = 16, } } diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSnapshot.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSnapshot.cs index cf091a4f197..8c1f3ad69df 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSnapshot.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSnapshot.cs @@ -1,6 +1,7 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System; using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.Language; @@ -15,12 +16,16 @@ internal abstract class ProjectSnapshot public abstract string FilePath { get; } + [Obsolete] public abstract bool IsInitialized { get; } public abstract VersionStamp Version { get; } + [Obsolete] public abstract Project WorkspaceProject { get; } + public virtual IReadOnlyList TagHelpers { get; } + public abstract RazorProjectEngine GetProjectEngine(); public abstract DocumentSnapshot GetDocument(string filePath); @@ -36,8 +41,10 @@ internal abstract class ProjectSnapshot /// A list of related documents. public abstract IEnumerable GetRelatedDocuments(DocumentSnapshot document); + [Obsolete("Use the " + nameof(TagHelpers) + " propery instead")] public abstract Task> GetTagHelpersAsync(); + [Obsolete("Use the " + nameof(TagHelpers) + " propery instead")] public abstract bool TryGetTagHelpers(out IReadOnlyList result); } } \ No newline at end of file diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSnapshotManagerBase.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSnapshotManagerBase.cs index 5545bbb3a8d..c2068a751a6 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSnapshotManagerBase.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSnapshotManagerBase.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using Microsoft.AspNetCore.Razor.Language; using Microsoft.CodeAnalysis.Text; namespace Microsoft.CodeAnalysis.Razor.ProjectSystem @@ -12,7 +13,6 @@ internal abstract class ProjectSnapshotManagerBase : ProjectSnapshotManager public abstract void DocumentAdded(HostProject hostProject, HostDocument hostDocument, TextLoader textLoader); - // Yeah this is kinda ugly. public abstract void DocumentOpened(string projectFilePath, string documentFilePath, SourceText sourceText); public abstract void DocumentClosed(string projectFilePath, string documentFilePath, TextLoader textLoader); @@ -23,24 +23,18 @@ internal abstract class ProjectSnapshotManagerBase : ProjectSnapshotManager public abstract void DocumentRemoved(HostProject hostProject, HostDocument hostDocument); - public abstract void HostProjectAdded(HostProject hostProject); + public abstract void ProjectAdded(HostProject hostProject); - public abstract void HostProjectChanged(HostProject hostProject); + public abstract void ProjectConfigurationChanged(HostProject hostProject); - public abstract void HostProjectRemoved(HostProject hostProject); + public abstract void ProjectWorkspaceStateChanged(string projectFilePath, ProjectWorkspaceState projectWorkspaceState); - public abstract void WorkspaceProjectAdded(Project workspaceProject); - - public abstract void WorkspaceProjectChanged(Project workspaceProject); - - public abstract void WorkspaceProjectRemoved(Project workspaceProject); + public abstract void ProjectRemoved(HostProject hostProject); public abstract void ReportError(Exception exception); public abstract void ReportError(Exception exception, ProjectSnapshot project); public abstract void ReportError(Exception exception, HostProject hostProject); - - public abstract void ReportError(Exception exception, Project workspaceProject); } } \ No newline at end of file diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectState.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectState.cs index 49a07bd83ad..d84036888f6 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectState.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectState.cs @@ -16,13 +16,11 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem // Internal tracker for DefaultProjectSnapshot internal class ProjectState { - private const ProjectDifference ClearComputedStateMask = ProjectDifference.ConfigurationChanged; + private const ProjectDifference ClearConfigurationVersionMask = ProjectDifference.ConfigurationChanged; - private const ProjectDifference ClearCachedTagHelpersMask = + private const ProjectDifference ClearProjectWorkspaceStateVersionMask = ProjectDifference.ConfigurationChanged | - ProjectDifference.WorkspaceProjectAdded | - ProjectDifference.WorkspaceProjectChanged | - ProjectDifference.WorkspaceProjectRemoved; + ProjectDifference.ProjectWorkspaceStateChanged; private const ProjectDifference ClearDocumentCollectionVersionMask = ProjectDifference.ConfigurationChanged | @@ -31,11 +29,15 @@ internal class ProjectState private static readonly ImmutableDictionary EmptyDocuments = ImmutableDictionary.Create(FilePathComparer.Instance); private static readonly ImmutableDictionary> EmptyImportsToRelatedDocuments = ImmutableDictionary.Create>(FilePathComparer.Instance); + private static readonly IReadOnlyList EmptyTagHelpers = Array.Empty(); private readonly object _lock; - private ComputedStateTracker _computedState; + private RazorProjectEngine _projectEngine; - public static ProjectState Create(HostWorkspaceServices services, HostProject hostProject, Project workspaceProject = null) + public static ProjectState Create( + HostWorkspaceServices services, + HostProject hostProject, + ProjectWorkspaceState projectWorkspaceState = null) { if (services == null) { @@ -47,17 +49,17 @@ public static ProjectState Create(HostWorkspaceServices services, HostProject ho throw new ArgumentNullException(nameof(hostProject)); } - return new ProjectState(services, hostProject, workspaceProject); + return new ProjectState(services, hostProject, projectWorkspaceState); } private ProjectState( HostWorkspaceServices services, HostProject hostProject, - Project workspaceProject) + ProjectWorkspaceState projectWorkspaceState) { Services = services; HostProject = hostProject; - WorkspaceProject = workspaceProject; + ProjectWorkspaceState = projectWorkspaceState; Documents = EmptyDocuments; ImportsToRelatedDocuments = EmptyImportsToRelatedDocuments; Version = VersionStamp.Create(); @@ -70,7 +72,7 @@ private ProjectState( ProjectState older, ProjectDifference difference, HostProject hostProject, - Project workspaceProject, + ProjectWorkspaceState projectWorkspaceState, ImmutableDictionary documents, ImmutableDictionary> importsToRelatedDocuments) { @@ -98,7 +100,7 @@ private ProjectState( Version = older.Version.GetNewerVersion(); HostProject = hostProject; - WorkspaceProject = workspaceProject; + ProjectWorkspaceState = projectWorkspaceState; Documents = documents; ImportsToRelatedDocuments = importsToRelatedDocuments; @@ -114,16 +116,26 @@ private ProjectState( DocumentCollectionVersion = Version; } - if ((difference & ClearComputedStateMask) == 0 && older._computedState != null) + if ((difference & ClearConfigurationVersionMask) == 0 && older._projectEngine != null) { // Optimistically cache the RazorProjectEngine. - _computedState = new ComputedStateTracker(this, older._computedState); + _projectEngine = older.ProjectEngine; + ConfigurationVersion = older.ConfigurationVersion; } + else + { + ConfigurationVersion = Version; + } - if ((difference & ClearCachedTagHelpersMask) == 0 && _computedState != null) + if ((difference & ClearProjectWorkspaceStateVersionMask) == 0 || + ProjectWorkspaceState == older.ProjectWorkspaceState || + ProjectWorkspaceState?.Equals(older.ProjectWorkspaceState) == true) { - // It's OK to keep the computed Tag Helpers. - _computedState.TaskUnsafe = older._computedState?.TaskUnsafe; + ProjectWorkspaceStateVersion = older.ProjectWorkspaceStateVersion; + } + else + { + ProjectWorkspaceStateVersion = Version; } } @@ -135,9 +147,11 @@ private ProjectState( public HostProject HostProject { get; } + public ProjectWorkspaceState ProjectWorkspaceState { get; } + public HostWorkspaceServices Services { get; } - public Project WorkspaceProject { get; } + public IReadOnlyList TagHelpers => ProjectWorkspaceState?.TagHelpers ?? EmptyTagHelpers; /// /// Gets the version of this project, INCLUDING content changes. The is @@ -152,56 +166,31 @@ private ProjectState( /// public VersionStamp DocumentCollectionVersion { get; } - public RazorProjectEngine ProjectEngine => ComputedState.ProjectEngine; - - public bool IsTagHelperResultAvailable => ComputedState.TaskUnsafe?.IsCompleted == true; - - private ComputedStateTracker ComputedState + public RazorProjectEngine ProjectEngine { get { - if (_computedState == null) + lock (_lock) { - lock (_lock) + if (_projectEngine == null) { - if (_computedState == null) - { - _computedState = new ComputedStateTracker(this); - } + _projectEngine = this.CreateProjectEngine(); } } - return _computedState; + return _projectEngine; } + } /// - /// Gets the version of this project based on the computed state, NOT INCLUDING content + /// Gets the version of this project based on the project workspace state, NOT INCLUDING content /// changes. The computed state is guaranteed to change when the configuration or tag helpers /// change. /// - /// Asynchronously returns the computed version. - public async Task GetComputedStateVersionAsync(ProjectSnapshot snapshot) - { - if (snapshot == null) - { - throw new ArgumentNullException(nameof(snapshot)); - } - - var (_, version) = await ComputedState.GetTagHelpersAndVersionAsync(snapshot).ConfigureAwait(false); - return version; - } + public VersionStamp ProjectWorkspaceStateVersion { get; } - public async Task> GetTagHelpersAsync(ProjectSnapshot snapshot) - { - if (snapshot == null) - { - throw new ArgumentNullException(nameof(snapshot)); - } - - var (tagHelpers, _) = await ComputedState.GetTagHelpersAndVersionAsync(snapshot).ConfigureAwait(false); - return tagHelpers; - } + public VersionStamp ConfigurationVersion { get; } public ProjectState WithAddedHostDocument(HostDocument hostDocument, Func> loader) { @@ -238,7 +227,7 @@ public ProjectState WithAddedHostDocument(HostDocument hostDocument, Func kvp.Key, kvp => kvp.Value.WithWorkspaceProjectChange(), FilePathComparer.Instance); - var state = new ProjectState(this, difference, HostProject, workspaceProject, documents, ImportsToRelatedDocuments); + var documents = Documents.ToImmutableDictionary(kvp => kvp.Key, kvp => kvp.Value.WithProjectWorkspaceStateChange(), FilePathComparer.Instance); + var state = new ProjectState(this, difference, HostProject, projectWorkspaceState, documents, ImportsToRelatedDocuments); return state; } @@ -432,7 +402,7 @@ private RazorProjectEngine CreateProjectEngine() public List GetImportDocumentTargetPaths(string targetPath) { - var projectEngine = ComputedState.ProjectEngine; + var projectEngine = ProjectEngine; var importFeatures = projectEngine.ProjectFeatures.OfType(); var projectItem = projectEngine.FileSystem.GetItem(targetPath); var importItems = importFeatures.SelectMany(f => f.GetImports(projectItem)).Where(i => i.FilePath != null); @@ -447,90 +417,5 @@ public List GetImportDocumentTargetPaths(string targetPath) return targetPaths; } - - // ComputedStateTracker is the 'holder' of all of the state that can be cached based on - // the data in a ProjectState. It should not hold onto a ProjectState directly - // as that could lead to things being in memory longer than we want them to. - // - // Rather, a ComputedStateTracker instance can hold on to a previous instance from an older - // version of the same project. - private class ComputedStateTracker - { - // ProjectState.Version - private readonly VersionStamp _projectStateVersion; - private readonly object _lock; - - private ComputedStateTracker _older; // We be set to null when state is computed - public Task<(IReadOnlyList, VersionStamp)> TaskUnsafe; - - public ComputedStateTracker(ProjectState state, ComputedStateTracker older = null) - { - _projectStateVersion = state.Version; - _lock = state._lock; - _older = older; - - ProjectEngine = _older?.ProjectEngine; - if (ProjectEngine == null) - { - ProjectEngine = state.CreateProjectEngine(); - } - } - - public RazorProjectEngine ProjectEngine { get; } - - public Task<(IReadOnlyList, VersionStamp)> GetTagHelpersAndVersionAsync(ProjectSnapshot snapshot) - { - if (TaskUnsafe == null) - { - lock (_lock) - { - if (TaskUnsafe == null) - { - TaskUnsafe = GetTagHelpersAndVersionCoreAsync(snapshot); - } - } - } - - return TaskUnsafe; - } - - private async Task<(IReadOnlyList, VersionStamp)> GetTagHelpersAndVersionCoreAsync(ProjectSnapshot snapshot) - { - // Don't allow synchronous execution - we expect this to always be called with the lock. - await Task.Yield(); - - var services = ((DefaultProjectSnapshot)snapshot).State.Services; - var resolver = services.GetLanguageServices(RazorLanguage.Name).GetRequiredService(); - - var tagHelpers = (await resolver.GetTagHelpersAsync(snapshot).ConfigureAwait(false)).Descriptors; - if (_older?.TaskUnsafe != null) - { - // We have something to diff against. - var (olderTagHelpers, olderVersion) = await _older.TaskUnsafe.ConfigureAwait(false); - - var difference = new HashSet(TagHelperDescriptorComparer.Default); - difference.UnionWith(olderTagHelpers); - difference.SymmetricExceptWith(tagHelpers); - - if (difference.Count == 0) - { - lock (_lock) - { - - // Everything is the same. Return the cached version. - TaskUnsafe = _older.TaskUnsafe; - _older = null; - return (olderTagHelpers, olderVersion); - } - } - } - - lock (_lock) - { - _older = null; - return (tagHelpers, _projectStateVersion); - } - } - } } } diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectWorkspaceState.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectWorkspaceState.cs new file mode 100644 index 00000000000..7b9a8b05462 --- /dev/null +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectWorkspaceState.cs @@ -0,0 +1,59 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.Extensions.Internal; + +namespace Microsoft.CodeAnalysis.Razor.ProjectSystem +{ + public sealed class ProjectWorkspaceState : IEquatable + { + public static readonly ProjectWorkspaceState Default = new ProjectWorkspaceState(Array.Empty()); + + public ProjectWorkspaceState(IReadOnlyList tagHelpers) + { + if (tagHelpers == null) + { + throw new ArgumentNullException(nameof(tagHelpers)); + } + + TagHelpers = tagHelpers; + } + + public IReadOnlyList TagHelpers { get; } + + public override bool Equals(object obj) + { + return base.Equals(obj as ProjectWorkspaceState); + } + + public bool Equals(ProjectWorkspaceState other) + { + if (object.ReferenceEquals(other, null)) + { + return false; + } + + if (!Enumerable.SequenceEqual(TagHelpers, other.TagHelpers)) + { + return false; + } + + return true; + } + + public override int GetHashCode() + { + var hash = new HashCodeCombiner(); + for (var i = 0; i < TagHelpers.Count; i++) + { + hash.Add(TagHelpers[i].GetHashCode()); + } + + return hash.CombinedHash; + } + } +} \ No newline at end of file diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/WorkspaceProjectSnapshotChangeTrigger.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/WorkspaceProjectStateChangeDetector.cs similarity index 57% rename from src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/WorkspaceProjectSnapshotChangeTrigger.cs rename to src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/WorkspaceProjectStateChangeDetector.cs index 85f1f10df54..9f50b7c8551 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/WorkspaceProjectSnapshotChangeTrigger.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/WorkspaceProjectStateChangeDetector.cs @@ -5,17 +5,19 @@ using System.Collections.Generic; using System.Composition; using System.Diagnostics; -using System.IO; +using System.Linq; using System.Threading.Tasks; namespace Microsoft.CodeAnalysis.Razor.ProjectSystem { + [Shared] [Export(typeof(ProjectSnapshotChangeTrigger))] - internal class WorkspaceProjectSnapshotChangeTrigger : ProjectSnapshotChangeTrigger + internal class WorkspaceProjectStateChangeDetector : ProjectSnapshotChangeTrigger { + private readonly ProjectWorkspaceStateGenerator _workspaceStateGenerator; private ProjectSnapshotManagerBase _projectManager; - public int ProjectChangeDelay { get; set; } = 3 * 1000; + public int EnqueueDelay { get; set; } = 3 * 1000; // We throttle updates to projects to prevent doing too much work while the projects // are being initialized. @@ -23,26 +25,29 @@ internal class WorkspaceProjectSnapshotChangeTrigger : ProjectSnapshotChangeTrig // Internal for testing internal Dictionary _deferredUpdates; + [ImportingConstructor] + public WorkspaceProjectStateChangeDetector(ProjectWorkspaceStateGenerator workspaceStateGenerator) + { + if (workspaceStateGenerator == null) + { + throw new ArgumentNullException(nameof(workspaceStateGenerator)); + } + + _workspaceStateGenerator = workspaceStateGenerator; + } + public override void Initialize(ProjectSnapshotManagerBase projectManager) { _projectManager = projectManager; + _projectManager.Changed += ProjectManager_Changed; _projectManager.Workspace.WorkspaceChanged += Workspace_WorkspaceChanged; _deferredUpdates = new Dictionary(); + // This will usually no-op, in the case that another project snapshot change trigger immediately adds projects we want to be able to handle those projects InitializeSolution(_projectManager.Workspace.CurrentSolution); } - private void InitializeSolution(Solution solution) - { - Debug.Assert(solution != null); - - foreach (var project in solution.Projects) - { - _projectManager.WorkspaceProjectAdded(project); - } - } - // Internal for testing internal void Workspace_WorkspaceChanged(object sender, WorkspaceChangeEventArgs e) { @@ -52,16 +57,25 @@ internal void Workspace_WorkspaceChanged(object sender, WorkspaceChangeEventArgs case WorkspaceChangeKind.ProjectAdded: { project = e.NewSolution.GetProject(e.ProjectId); + Debug.Assert(project != null); - _projectManager.WorkspaceProjectAdded(project); + if (TryGetProjectSnapshot(project.FilePath, out var projectSnapshot)) + { + _workspaceStateGenerator.Update(project, projectSnapshot); + } break; } case WorkspaceChangeKind.ProjectChanged: case WorkspaceChangeKind.ProjectReloaded: { - EnqueueUpdate(e.ProjectId); + project = e.NewSolution.GetProject(e.ProjectId); + + if (TryGetProjectSnapshot(project?.FilePath, out var _)) + { + EnqueueUpdate(e.ProjectId); + } break; } @@ -70,7 +84,11 @@ internal void Workspace_WorkspaceChanged(object sender, WorkspaceChangeEventArgs project = e.OldSolution.GetProject(e.ProjectId); Debug.Assert(project != null); - _projectManager.WorkspaceProjectRemoved(project); + if (TryGetProjectSnapshot(project?.FilePath, out var projectSnapshot)) + { + _workspaceStateGenerator.Update(workspaceProject: null, projectSnapshot); + } + break; } @@ -82,7 +100,7 @@ internal void Workspace_WorkspaceChanged(object sender, WorkspaceChangeEventArgs // is saved, or loses focus in the editor. project = e.OldSolution.GetProject(e.ProjectId); var document = project.GetDocument(e.DocumentId); - + // Using EndsWith because Path.GetExtension will ignore everything before .cs // Using Ordinal because the SDK generates these filenames. if (document.FilePath != null && document.FilePath.EndsWith(".cshtml.g.cs", StringComparison.Ordinal)) @@ -103,7 +121,11 @@ internal void Workspace_WorkspaceChanged(object sender, WorkspaceChangeEventArgs { foreach (var p in e.OldSolution.Projects) { - _projectManager.WorkspaceProjectRemoved(p); + + if (TryGetProjectSnapshot(p?.FilePath, out var projectSnapshot)) + { + _workspaceStateGenerator.Update(workspaceProject: null, projectSnapshot); + } } } @@ -112,6 +134,36 @@ internal void Workspace_WorkspaceChanged(object sender, WorkspaceChangeEventArgs } } + private void InitializeSolution(Solution solution) + { + Debug.Assert(solution != null); + + foreach (var project in solution.Projects) + { + if (TryGetProjectSnapshot(project?.FilePath, out var projectSnapshot)) + { + _workspaceStateGenerator.Update(project, projectSnapshot); + } + } + } + + private void ProjectManager_Changed(object sender, ProjectChangeEventArgs args) + { + if (args.Kind == ProjectChangeKind.ProjectAdded) + { + var associatedWorkspaceProject = _projectManager + .Workspace + .CurrentSolution + .Projects + .FirstOrDefault(project => FilePathComparer.Instance.Equals(args.ProjectFilePath, project.FilePath)); + + if (associatedWorkspaceProject != null) + { + _workspaceStateGenerator.Update(associatedWorkspaceProject, args.Newer); + } + } + } + private void EnqueueUpdate(ProjectId projectId) { // A race is not possible here because we use the main thread to synchronize the updates @@ -124,14 +176,26 @@ private void EnqueueUpdate(ProjectId projectId) private async Task UpdateAfterDelay(ProjectId projectId) { - await Task.Delay(ProjectChangeDelay); + await Task.Delay(EnqueueDelay); var solution = _projectManager.Workspace.CurrentSolution; var workspaceProject = solution.GetProject(projectId); - if (workspaceProject != null) + if (workspaceProject != null && TryGetProjectSnapshot(workspaceProject.FilePath, out var projectSnapshot)) { - _projectManager.WorkspaceProjectChanged(workspaceProject); + _workspaceStateGenerator.Update(workspaceProject, projectSnapshot); } } + + private bool TryGetProjectSnapshot(string projectFilePath, out ProjectSnapshot projectSnapshot) + { + if (projectFilePath == null) + { + projectSnapshot = null; + return false; + } + + projectSnapshot = _projectManager.GetLoadedProject(projectFilePath); + return projectSnapshot != null; + } } } diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectWorkspaceStateGenerator.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectWorkspaceStateGenerator.cs new file mode 100644 index 00000000000..5f1be6ff597 --- /dev/null +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectWorkspaceStateGenerator.cs @@ -0,0 +1,12 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.CodeAnalysis.Razor.ProjectSystem; + +namespace Microsoft.CodeAnalysis.Razor +{ + internal abstract class ProjectWorkspaceStateGenerator : ProjectSnapshotChangeTrigger + { + public abstract void Update(Project workspaceProject, ProjectSnapshot projectSnapshot); + } +} \ No newline at end of file diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/TagHelperResolver.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/TagHelperResolver.cs index 4316f6ee3be..28f134571ae 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/TagHelperResolver.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/TagHelperResolver.cs @@ -14,13 +14,13 @@ namespace Microsoft.CodeAnalysis.Razor { internal abstract class TagHelperResolver : ILanguageService { - public abstract Task GetTagHelpersAsync(ProjectSnapshot project, CancellationToken cancellationToken = default); + public abstract Task GetTagHelpersAsync(Project workspaceProject, ProjectSnapshot projectSnapshot, CancellationToken cancellationToken = default); - protected virtual async Task GetTagHelpersAsync(ProjectSnapshot project, RazorProjectEngine engine) + protected virtual async Task GetTagHelpersAsync(Project workspaceProject, RazorProjectEngine engine) { - if (project == null) + if (workspaceProject == null) { - throw new ArgumentNullException(nameof(project)); + throw new ArgumentNullException(nameof(workspaceProject)); } if (engine == null) @@ -28,11 +28,6 @@ protected virtual async Task GetTagHelpersAsync(Proje throw new ArgumentNullException(nameof(engine)); } - if (project.WorkspaceProject == null) - { - return TagHelperResolutionResult.Empty; - } - var providers = engine.Engine.Features.OfType().ToArray(); if (providers.Length == 0) { @@ -44,7 +39,7 @@ protected virtual async Task GetTagHelpersAsync(Proje context.ExcludeHidden = true; context.IncludeDocumentation = true; - var compilation = await project.WorkspaceProject.GetCompilationAsync().ConfigureAwait(false); + var compilation = await workspaceProject.GetCompilationAsync().ConfigureAwait(false); if (CompilationTagHelperFeature.IsValidCompilation(compilation)) { context.SetCompilation(compilation); diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor/Properties/AssemblyInfo.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor/Properties/AssemblyInfo.cs index fc489b11deb..adee145e23e 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor/Properties/AssemblyInfo.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor/Properties/AssemblyInfo.cs @@ -16,5 +16,6 @@ [assembly: InternalsVisibleTo("Microsoft.CodeAnalysis.Remote.Razor, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] [assembly: InternalsVisibleTo("Microsoft.VisualStudio.Editor.Razor, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] [assembly: InternalsVisibleTo("Microsoft.VisualStudio.LanguageServices.Razor, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] +[assembly: InternalsVisibleTo("Microsoft.VisualStudio.Mac.LanguageServices.Razor, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] [assembly: InternalsVisibleTo("RazorDeveloperTools, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")] diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/RazorLanguageService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/RazorLanguageService.cs index 16ababe0db7..3c32f86ac13 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/RazorLanguageService.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/RazorLanguageService.cs @@ -3,6 +3,7 @@ using System; using System.IO; +using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.Razor; @@ -19,9 +20,18 @@ public RazorLanguageService(Stream stream, IServiceProvider serviceProvider) public async Task GetTagHelpersAsync(ProjectSnapshotHandle projectHandle, string factoryTypeName, CancellationToken cancellationToken = default) { - var project = await GetProjectSnapshotAsync(projectHandle, cancellationToken).ConfigureAwait(false); + var projectSnapshot = await GetProjectSnapshotAsync(projectHandle, cancellationToken).ConfigureAwait(false); + var solution = await GetSolutionAsync(cancellationToken); + var workspaceProject = solution + .Projects + .FirstOrDefault(project => FilePathComparer.Instance.Equals(project.FilePath, projectSnapshot.FilePath)); - return await RazorServices.TagHelperResolver.GetTagHelpersAsync(project, factoryTypeName, cancellationToken); + if (workspaceProject == null) + { + return TagHelperResolutionResult.Empty; + } + + return await RazorServices.TagHelperResolver.GetTagHelpersAsync(workspaceProject, projectHandle.Configuration, factoryTypeName, cancellationToken); } } } diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/RazorServiceBase.cs b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/RazorServiceBase.cs index 32e8e3bcb62..2e391265c83 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/RazorServiceBase.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/RazorServiceBase.cs @@ -29,28 +29,23 @@ public RazorServiceBase(Stream stream, IServiceProvider serviceProvider) protected RazorServices RazorServices { get; } - protected virtual async Task GetProjectSnapshotAsync(ProjectSnapshotHandle projectHandle, CancellationToken cancellationToken) + protected virtual Task GetProjectSnapshotAsync(ProjectSnapshotHandle projectHandle, CancellationToken cancellationToken) { if (projectHandle == null) { throw new ArgumentNullException(nameof(projectHandle)); } - var solution = await GetSolutionAsync(cancellationToken).ConfigureAwait(false); - var workspaceProject = solution.GetProject(projectHandle.WorkspaceProjectId); - - return new SerializedProjectSnapshot(projectHandle.FilePath, projectHandle.Configuration, workspaceProject); + return Task.FromResult(new SerializedProjectSnapshot(projectHandle.FilePath, projectHandle.Configuration)); } private class SerializedProjectSnapshot : ProjectSnapshot { - public SerializedProjectSnapshot(string filePath, RazorConfiguration configuration, Project workspaceProject) + public SerializedProjectSnapshot(string filePath, RazorConfiguration configuration) { FilePath = filePath; Configuration = configuration; - WorkspaceProject = workspaceProject; - IsInitialized = true; Version = VersionStamp.Default; } @@ -60,11 +55,13 @@ public SerializedProjectSnapshot(string filePath, RazorConfiguration configurati public override string FilePath { get; } - public override bool IsInitialized { get; } public override VersionStamp Version { get; } +#pragma warning disable CS0672 // Member overrides obsolete member + public override bool IsInitialized { get; } public override Project WorkspaceProject { get; } +#pragma warning restore CS0672 // Member overrides obsolete member public override DocumentSnapshot GetDocument(string filePath) { @@ -91,6 +88,7 @@ public override RazorProjectEngine GetProjectEngine() throw new NotImplementedException(); } +#pragma warning disable CS0672 // Member overrides obsolete member public override Task> GetTagHelpersAsync() { throw new NotImplementedException(); @@ -100,6 +98,7 @@ public override bool TryGetTagHelpers(out IReadOnlyList res { throw new NotImplementedException(); } +#pragma warning restore CS0672 // Member overrides obsolete member } } } diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/RemoteTagHelperResolver.cs b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/RemoteTagHelperResolver.cs index f4ccaf77550..9e50b5b5adf 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/RemoteTagHelperResolver.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/RemoteTagHelperResolver.cs @@ -25,28 +25,32 @@ public RemoteTagHelperResolver(IFallbackProjectEngineFactory fallbackFactory) _fallbackFactory = fallbackFactory; } - public override Task GetTagHelpersAsync(ProjectSnapshot project, CancellationToken cancellationToken = default) + public override Task GetTagHelpersAsync(Project project, ProjectSnapshot projectSnapshot, CancellationToken cancellationToken = default) { throw new NotImplementedException(); } - public Task GetTagHelpersAsync(ProjectSnapshot project, string factoryTypeName, CancellationToken cancellationToken = default) + public Task GetTagHelpersAsync( + Project project, + RazorConfiguration configuration, + string factoryTypeName, + CancellationToken cancellationToken = default) { if (project == null) { throw new ArgumentNullException(nameof(project)); } - if (project.Configuration == null || project.WorkspaceProject == null) + if (configuration == null || project == null) { return Task.FromResult(TagHelperResolutionResult.Empty); } - var engine = CreateProjectEngine(project, factoryTypeName); + var engine = CreateProjectEngine(configuration, factoryTypeName); return GetTagHelpersAsync(project, engine); } - internal RazorProjectEngine CreateProjectEngine(ProjectSnapshot project, string factoryTypeName) + internal RazorProjectEngine CreateProjectEngine(RazorConfiguration configuration, string factoryTypeName) { // This section is really similar to the code DefaultProjectEngineFactoryService // but with a few differences that are significant in the remote scenario @@ -57,7 +61,7 @@ internal RazorProjectEngine CreateProjectEngine(ProjectSnapshot project, string // The default configuration currently matches MVC-2.0. Beyond MVC-2.0 we added SDK support for // properly detecting project versions, so that's a good version to assume when we can't find a // configuration. - var configuration = project?.Configuration ?? DefaultConfiguration; + configuration = configuration ?? DefaultConfiguration; // If there's no factory to handle the configuration then fall back to a very basic configuration. // diff --git a/src/Razor/src/Microsoft.VisualStudio.Editor.Razor/DefaultTagHelperResolver.cs b/src/Razor/src/Microsoft.VisualStudio.Editor.Razor/DefaultTagHelperResolver.cs index 400a0ce2b8c..6fe44738fe1 100644 --- a/src/Razor/src/Microsoft.VisualStudio.Editor.Razor/DefaultTagHelperResolver.cs +++ b/src/Razor/src/Microsoft.VisualStudio.Editor.Razor/DefaultTagHelperResolver.cs @@ -4,6 +4,7 @@ using System; using System.Threading; using System.Threading.Tasks; +using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Razor; using Microsoft.CodeAnalysis.Razor.ProjectSystem; @@ -11,19 +12,24 @@ namespace Microsoft.VisualStudio.Editor.Razor { internal class DefaultTagHelperResolver : TagHelperResolver { - public override Task GetTagHelpersAsync(ProjectSnapshot project, CancellationToken cancellationToken = default) + public override Task GetTagHelpersAsync(Project workspaceProject, ProjectSnapshot projectSnapshot, CancellationToken cancellationToken = default) { - if (project == null) + if (workspaceProject == null) { - throw new ArgumentNullException(nameof(project)); + throw new ArgumentNullException(nameof(workspaceProject)); } - if (project.Configuration == null || project.WorkspaceProject == null) + if (projectSnapshot == null) + { + throw new ArgumentNullException(nameof(projectSnapshot)); + } + + if (projectSnapshot.Configuration == null) { return Task.FromResult(TagHelperResolutionResult.Empty); } - return GetTagHelpersAsync(project, project.GetProjectEngine()); + return GetTagHelpersAsync(workspaceProject, projectSnapshot.GetProjectEngine()); } } } diff --git a/src/Razor/src/Microsoft.VisualStudio.Editor.Razor/DefaultVisualStudioDocumentTracker.cs b/src/Razor/src/Microsoft.VisualStudio.Editor.Razor/DefaultVisualStudioDocumentTracker.cs index 02311c217be..1faebe1698c 100644 --- a/src/Razor/src/Microsoft.VisualStudio.Editor.Razor/DefaultVisualStudioDocumentTracker.cs +++ b/src/Razor/src/Microsoft.VisualStudio.Editor.Razor/DefaultVisualStudioDocumentTracker.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.Language; @@ -31,12 +32,6 @@ internal class DefaultVisualStudioDocumentTracker : VisualStudioDocumentTracker private ProjectSnapshot _projectSnapshot; private int _subscribeCount; - // 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 _tagHelpers; - public override event EventHandler ContextChanged; public DefaultVisualStudioDocumentTracker( @@ -99,22 +94,16 @@ public DefaultVisualStudioDocumentTracker( _workspace = workspace; // For now we assume that the workspace is the always default VS workspace. _textViews = new List(); - _tagHelpers = Array.Empty(); } public override RazorConfiguration Configuration => _projectSnapshot?.Configuration; public override EditorSettings EditorSettings => _workspaceEditorSettings.Current; - public override IReadOnlyList TagHelpers => _tagHelpers; + public override IReadOnlyList TagHelpers => ProjectSnapshot?.TagHelpers; public override bool IsSupportedProject => _isSupportedProject; - public override Project Project => - _projectSnapshot.WorkspaceProject == null ? - null : - _workspace.CurrentSolution.GetProject(_projectSnapshot.WorkspaceProject.Id); - internal override ProjectSnapshot ProjectSnapshot => _projectSnapshot; public override ITextBuffer TextBuffer => _textBuffer; @@ -127,8 +116,6 @@ public DefaultVisualStudioDocumentTracker( public override string ProjectPath => _projectPath; - public Task PendingTagHelperTask => _computingTagHelpers.task ?? Task.CompletedTask; - internal void AddTextView(ITextView textView) { if (textView == null) @@ -216,51 +203,6 @@ public void Unsubscribe() OnContextChanged(kind: ContextChangeKind.ProjectChanged); } - private void StartComputingTagHelpers() - { - _foregroundDispatcher.AssertForegroundThread(); - - 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> task) - { - _foregroundDispatcher.AssertForegroundThread(); - - Debug.Assert(_computingTagHelpers.project != null && _computingTagHelpers.task != null); - - if (!_isSupportedProject) - { - return; - } - - _tagHelpers = task.Exception == null ? task.Result : Array.Empty(); - 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(); @@ -270,13 +212,6 @@ private void OnContextChanged(ContextChangeKind kind) { handler(this, new ContextChangeEventArgs(kind)); } - - if (kind == ContextChangeKind.ProjectChanged && - _projectSnapshot != null && - _computingTagHelpers.project == null) - { - StartComputingTagHelpers(); - } } // Internal for testing @@ -304,6 +239,12 @@ internal void ProjectManager_Changed(object sender, ProjectChangeEventArgs e) // Just an update OnContextChanged(ContextChangeKind.ProjectChanged); + + if (e.Older == null || + !Enumerable.SequenceEqual(e.Older.TagHelpers, e.Newer.TagHelpers)) + { + OnContextChanged(ContextChangeKind.TagHelpersChanged); + } break; case ProjectChangeKind.ProjectRemoved: diff --git a/src/Razor/src/Microsoft.VisualStudio.Editor.Razor/VisualStudioDocumentTracker.cs b/src/Razor/src/Microsoft.VisualStudio.Editor.Razor/VisualStudioDocumentTracker.cs index 57c46bfc526..f0ca9de76fd 100644 --- a/src/Razor/src/Microsoft.VisualStudio.Editor.Razor/VisualStudioDocumentTracker.cs +++ b/src/Razor/src/Microsoft.VisualStudio.Editor.Razor/VisualStudioDocumentTracker.cs @@ -28,8 +28,6 @@ public abstract class VisualStudioDocumentTracker public abstract string ProjectPath { get; } - public abstract Project Project { get; } - internal abstract ProjectSnapshot ProjectSnapshot { get; } public abstract Workspace Workspace { get; } diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/OOPTagHelperResolver.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/OOPTagHelperResolver.cs index 2663f746dd1..564d709d13c 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/OOPTagHelperResolver.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/OOPTagHelperResolver.cs @@ -44,14 +44,19 @@ public OOPTagHelperResolver(ProjectSnapshotProjectEngineFactory factory, ErrorRe _defaultResolver = new DefaultTagHelperResolver(); } - public override async Task GetTagHelpersAsync(ProjectSnapshot project, CancellationToken cancellationToken = default) + public override async Task GetTagHelpersAsync(Project workspaceProject, ProjectSnapshot projectSnapshot, CancellationToken cancellationToken = default) { - if (project == null) + if (workspaceProject == null) { - throw new ArgumentNullException(nameof(project)); + throw new ArgumentNullException(nameof(workspaceProject)); } - if (project.Configuration == null || project.WorkspaceProject == null) + if (projectSnapshot == null) + { + throw new ArgumentNullException(nameof(projectSnapshot)); + } + + if (projectSnapshot.Configuration == null) { return TagHelperResolutionResult.Empty; } @@ -63,20 +68,20 @@ public override async Task GetTagHelpersAsync(Project // 3. Use fallback factory in process // // Calling into RazorTemplateEngineFactoryService.Create will accomplish #2 and #3 in one step. - var factory = _factory.FindSerializableFactory(project); + var factory = _factory.FindSerializableFactory(projectSnapshot); try { TagHelperResolutionResult result = null; if (factory != null) { - result = await ResolveTagHelpersOutOfProcessAsync(factory, project).ConfigureAwait(false); + result = await ResolveTagHelpersOutOfProcessAsync(factory, workspaceProject, projectSnapshot).ConfigureAwait(false); } if (result == null) { // Was unable to get tag helpers OOP, fallback to default behavior. - result = await ResolveTagHelpersInProcessAsync(project).ConfigureAwait(false); + result = await ResolveTagHelpersInProcessAsync(workspaceProject, projectSnapshot).ConfigureAwait(false); } return result; @@ -91,7 +96,7 @@ public override async Task GetTagHelpersAsync(Project } } - protected virtual async Task ResolveTagHelpersOutOfProcessAsync(IProjectEngineFactory factory, ProjectSnapshot project) + protected virtual async Task ResolveTagHelpersOutOfProcessAsync(IProjectEngineFactory factory, Project workspaceProject, ProjectSnapshot projectSnapshot) { // We're being overly defensive here because the OOP host can return null for the client/session/operation // when it's disconnected (user stops the process). @@ -102,13 +107,13 @@ protected virtual async Task ResolveTagHelpersOutOfPr var client = await RazorLanguageServiceClientFactory.CreateAsync(_workspace, CancellationToken.None).ConfigureAwait(false); if (client != null) { - using (var session = await client.CreateSessionAsync(project.WorkspaceProject.Solution).ConfigureAwait(false)) + using (var session = await client.CreateSessionAsync(workspaceProject.Solution).ConfigureAwait(false)) { if (session != null) { var args = new object[] { - Serialize(project), + Serialize(projectSnapshot), factory == null ? null : factory.GetType().AssemblyQualifiedName, }; @@ -123,15 +128,15 @@ protected virtual async Task ResolveTagHelpersOutOfPr // We silence exceptions from the OOP host because we don't want to bring down VS for an OOP failure. // We will retry all failures in process anyway, so if there's a real problem that isn't unique to OOP // then it will report a crash in VS. - _errorReporter.ReportError(ex, project); + _errorReporter.ReportError(ex, projectSnapshot); } return null; } - protected virtual Task ResolveTagHelpersInProcessAsync(ProjectSnapshot project) + protected virtual Task ResolveTagHelpersInProcessAsync(Project project, ProjectSnapshot projectSnapshot) { - return _defaultResolver.GetTagHelpersAsync(project); + return _defaultResolver.GetTagHelpersAsync(project, projectSnapshot); } private static JObject Serialize(ProjectSnapshot snapshot) diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/RazorDynamicFileInfoProvider.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/RazorDynamicFileInfoProvider.cs index b826878732d..458025adc89 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/RazorDynamicFileInfoProvider.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/RazorDynamicFileInfoProvider.cs @@ -39,11 +39,11 @@ public RazorDynamicFileInfoProvider(DocumentServiceProviderFactory factory) public event EventHandler Updated; // Called by us to update entries - public void UpdateFileInfo(ProjectSnapshot project, DocumentSnapshot document) + public void UpdateFileInfo(ProjectSnapshot projectSnapshot, DocumentSnapshot document) { - if (project == null) + if (projectSnapshot == null) { - throw new ArgumentNullException(nameof(project)); + throw new ArgumentNullException(nameof(projectSnapshot)); } if (document == null) @@ -51,16 +51,10 @@ public void UpdateFileInfo(ProjectSnapshot project, DocumentSnapshot document) throw new ArgumentNullException(nameof(document)); } - if (project.WorkspaceProject == null) - { - // Don't bother if this isn't assocated with a project. - return; - } - // There's a possible race condition here where we're processing an update // and the project is getting unloaded. So if we don't find an entry we can // just ignore it. - var key = new Key(project.WorkspaceProject.Id, project.WorkspaceProject.FilePath, document.FilePath); + var key = new Key(projectSnapshot.FilePath, document.FilePath); if (_entries.TryGetValue(key, out var entry)) { lock (entry.Lock) @@ -85,16 +79,10 @@ public void SuppressDocument(ProjectSnapshot project, DocumentSnapshot document) throw new ArgumentNullException(nameof(document)); } - if (project.WorkspaceProject == null) - { - // Don't bother if this isn't assocated with a project. - return; - } - // There's a possible race condition here where we're processing an update // and the project is getting unloaded. So if we don't find an entry we can // just ignore it. - var key = new Key(project.WorkspaceProject.Id, project.WorkspaceProject.FilePath, document.FilePath); + var key = new Key(project.FilePath, document.FilePath); if (_entries.TryGetValue(key, out var entry)) { var updated = false; @@ -126,7 +114,7 @@ public Task GetDynamicFileInfoAsync(ProjectId projectId, string throw new ArgumentNullException(nameof(filePath)); } - var key = new Key(projectId, projectFilePath, filePath); + var key = new Key(projectFilePath, filePath); var entry = _entries.GetOrAdd(key, _createEmptyEntry); return Task.FromResult(entry.Current); } @@ -143,7 +131,7 @@ public Task RemoveDynamicFileInfoAsync(ProjectId projectId, string projectFilePa throw new ArgumentNullException(nameof(filePath)); } - var key = new Key(projectId, projectFilePath, filePath); + var key = new Key(projectFilePath, filePath); _entries.TryRemove(key, out var entry); return Task.CompletedTask; } @@ -207,13 +195,11 @@ public override string ToString() private readonly struct Key : IEquatable { - public readonly ProjectId ProjectId; public readonly string ProjectFilePath; public readonly string FilePath; - public Key(ProjectId projectId, string projectFilePath, string filePath) + public Key(string projectFilePath, string filePath) { - ProjectId = projectId; ProjectFilePath = projectFilePath; FilePath = filePath; } diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/RazorProjectHostBase.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/RazorProjectHostBase.cs index d6022083761..3fd2c10deec 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/RazorProjectHostBase.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/RazorProjectHostBase.cs @@ -178,16 +178,16 @@ protected void UpdateProjectUnsafe(HostProject project) } else if (_current == null && project != null) { - projectManager.HostProjectAdded(project); + projectManager.ProjectAdded(project); } else if (_current != null && project == null) { Debug.Assert(_currentDocuments.Count == 0); - projectManager.HostProjectRemoved(_current); + projectManager.ProjectRemoved(_current); } else { - projectManager.HostProjectChanged(project); + projectManager.ProjectConfigurationChanged(project); } _current = project; diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/Serialization/ProjectSnapshotHandle.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/Serialization/ProjectSnapshotHandle.cs index d186db2ec82..3aa46c00085 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/Serialization/ProjectSnapshotHandle.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/Serialization/ProjectSnapshotHandle.cs @@ -8,7 +8,9 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem { internal sealed class ProjectSnapshotHandle { - public ProjectSnapshotHandle(string filePath, RazorConfiguration configuration, ProjectId workspaceProjectId) + public ProjectSnapshotHandle( + string filePath, + RazorConfiguration configuration) { if (filePath == null) { @@ -17,13 +19,10 @@ public ProjectSnapshotHandle(string filePath, RazorConfiguration configuration, FilePath = filePath; Configuration = configuration; - WorkspaceProjectId = workspaceProjectId; } public RazorConfiguration Configuration { get; } public string FilePath { get; } - - public ProjectId WorkspaceProjectId { get; } } } diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/Serialization/ProjectSnapshotHandleJsonConverter.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/Serialization/ProjectSnapshotHandleJsonConverter.cs index 5b72763912a..4bfe587921a 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/Serialization/ProjectSnapshotHandleJsonConverter.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/Serialization/ProjectSnapshotHandleJsonConverter.cs @@ -30,10 +30,7 @@ public override object ReadJson(JsonReader reader, Type objectType, object exist var filePath = obj[nameof(ProjectSnapshotHandle.FilePath)].Value(); var configuration = obj[nameof(ProjectSnapshotHandle.Configuration)].ToObject(serializer); - var id = obj[nameof(ProjectSnapshotHandle.WorkspaceProjectId)].Value(); - var workspaceProjectId = id == null ? null : ProjectId.CreateFromSerialized(Guid.Parse(id)); - - return new ProjectSnapshotHandle(filePath, configuration, workspaceProjectId); + return new ProjectSnapshotHandle(filePath, configuration); } public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) @@ -56,17 +53,6 @@ public override void WriteJson(JsonWriter writer, object value, JsonSerializer s serializer.Serialize(writer, handle.Configuration); } - if (handle.WorkspaceProjectId == null) - { - writer.WritePropertyName(nameof(ProjectSnapshotHandle.WorkspaceProjectId)); - writer.WriteNull(); - } - else - { - writer.WritePropertyName(nameof(ProjectSnapshotHandle.WorkspaceProjectId)); - writer.WriteValue(handle.WorkspaceProjectId.Id); - } - writer.WriteEndObject(); } } diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/Serialization/ProjectSnapshotJsonConverter.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/Serialization/ProjectSnapshotJsonConverter.cs index 988d42b44dc..a871ce32ac2 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/Serialization/ProjectSnapshotJsonConverter.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/Serialization/ProjectSnapshotJsonConverter.cs @@ -32,7 +32,7 @@ public override object ReadJson(JsonReader reader, Type objectType, object exist public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { var project = (ProjectSnapshot)value; - var handle = new ProjectSnapshotHandle(project.FilePath, project.Configuration, project.WorkspaceProject?.Id); + var handle = new ProjectSnapshotHandle(project.FilePath, project.Configuration); ProjectSnapshotHandleJsonConverter.Instance.WriteJson(writer, handle, serializer); } diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/VsSolutionUpdatesProjectSnapshotChangeTrigger.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/VsSolutionUpdatesProjectSnapshotChangeTrigger.cs index 92c0ac80a0c..9053b8e4e95 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/VsSolutionUpdatesProjectSnapshotChangeTrigger.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/VsSolutionUpdatesProjectSnapshotChangeTrigger.cs @@ -3,7 +3,9 @@ using System; using System.ComponentModel.Composition; +using System.Linq; using System.Runtime.InteropServices; +using Microsoft.CodeAnalysis.Razor; using Microsoft.CodeAnalysis.Razor.ProjectSystem; using Microsoft.VisualStudio.Editor.Razor; using Microsoft.VisualStudio.Shell; @@ -16,13 +18,14 @@ internal class VsSolutionUpdatesProjectSnapshotChangeTrigger : ProjectSnapshotCh { private readonly IServiceProvider _services; private readonly TextBufferProjectService _projectService; - + private readonly ProjectWorkspaceStateGenerator _workspaceStateGenerator; private ProjectSnapshotManagerBase _projectManager; [ImportingConstructor] public VsSolutionUpdatesProjectSnapshotChangeTrigger( [Import(typeof(SVsServiceProvider))] IServiceProvider services, - TextBufferProjectService projectService) + TextBufferProjectService projectService, + ProjectWorkspaceStateGenerator workspaceStateGenerator) { if (services == null) { @@ -34,8 +37,14 @@ public VsSolutionUpdatesProjectSnapshotChangeTrigger( throw new ArgumentNullException(nameof(projectService)); } + if (workspaceStateGenerator == null) + { + throw new ArgumentNullException(nameof(workspaceStateGenerator)); + } + _services = services; _projectService = projectService; + _workspaceStateGenerator = workspaceStateGenerator; } public override void Initialize(ProjectSnapshotManagerBase projectManager) @@ -86,15 +95,16 @@ public int UpdateProjectCfg_Begin(IVsHierarchy pHierProj, IVsCfg pCfgProj, IVsCf public int UpdateProjectCfg_Done(IVsHierarchy pHierProj, IVsCfg pCfgProj, IVsCfg pCfgSln, uint dwAction, int fSuccess, int fCancel) { var projectPath = _projectService.GetProjectPath(pHierProj); - var project = _projectManager.GetLoadedProject(projectPath); - if (project != null && project.WorkspaceProject != null) + var projectSnapshot = _projectManager.GetLoadedProject(projectPath); + if (projectSnapshot != null) { - var workspaceProject = _projectManager.Workspace.CurrentSolution.GetProject(project.WorkspaceProject.Id); + var workspaceProject = _projectManager.Workspace.CurrentSolution.Projects.FirstOrDefault( + wp => FilePathComparer.Instance.Equals(wp.FilePath, projectSnapshot.FilePath)); if (workspaceProject != null) { // Trigger a tag helper update by forcing the project manager to see the workspace Project // from the current solution. - _projectManager.WorkspaceProjectChanged(workspaceProject); + _workspaceStateGenerator.Update(workspaceProject, projectSnapshot); } } diff --git a/src/Razor/src/Microsoft.VisualStudio.Mac.LanguageServices.Razor/ProjectBuildChangeTrigger.cs b/src/Razor/src/Microsoft.VisualStudio.Mac.LanguageServices.Razor/ProjectBuildChangeTrigger.cs index 8dc8d94aa41..0800df8f350 100644 --- a/src/Razor/src/Microsoft.VisualStudio.Mac.LanguageServices.Razor/ProjectBuildChangeTrigger.cs +++ b/src/Razor/src/Microsoft.VisualStudio.Mac.LanguageServices.Razor/ProjectBuildChangeTrigger.cs @@ -3,6 +3,7 @@ using System; using System.ComponentModel.Composition; +using System.Linq; using Microsoft.CodeAnalysis.Razor; using Microsoft.CodeAnalysis.Razor.ProjectSystem; using Microsoft.VisualStudio.Editor.Razor; @@ -15,11 +16,15 @@ namespace Microsoft.VisualStudio.Mac.LanguageServices.Razor internal class ProjectBuildChangeTrigger : ProjectSnapshotChangeTrigger { private readonly TextBufferProjectService _projectService; + private readonly ProjectWorkspaceStateGenerator _workspaceStateGenerator; private readonly ForegroundDispatcher _foregroundDispatcher; private ProjectSnapshotManagerBase _projectManager; [ImportingConstructor] - public ProjectBuildChangeTrigger(ForegroundDispatcher foregroundDispatcher, TextBufferProjectService projectService) + public ProjectBuildChangeTrigger( + ForegroundDispatcher foregroundDispatcher, + TextBufferProjectService projectService, + ProjectWorkspaceStateGenerator workspaceStateGenerator) { if (foregroundDispatcher == null) { @@ -31,14 +36,21 @@ public ProjectBuildChangeTrigger(ForegroundDispatcher foregroundDispatcher, Text throw new ArgumentNullException(nameof(projectService)); } + if (workspaceStateGenerator == null) + { + throw new ArgumentNullException(nameof(workspaceStateGenerator)); + } + _foregroundDispatcher = foregroundDispatcher; _projectService = projectService; + _workspaceStateGenerator = workspaceStateGenerator; } // Internal for testing internal ProjectBuildChangeTrigger( ForegroundDispatcher foregroundDispatcher, TextBufferProjectService projectService, + ProjectWorkspaceStateGenerator workspaceStateGenerator, ProjectSnapshotManagerBase projectManager) { if (foregroundDispatcher == null) @@ -51,6 +63,11 @@ internal ProjectBuildChangeTrigger( throw new ArgumentNullException(nameof(projectService)); } + if (workspaceStateGenerator == null) + { + throw new ArgumentNullException(nameof(workspaceStateGenerator)); + } + if (projectManager == null) { throw new ArgumentNullException(nameof(projectManager)); @@ -59,6 +76,7 @@ internal ProjectBuildChangeTrigger( _foregroundDispatcher = foregroundDispatcher; _projectService = projectService; _projectManager = projectManager; + _workspaceStateGenerator = workspaceStateGenerator; } public override void Initialize(ProjectSnapshotManagerBase projectManager) @@ -97,15 +115,16 @@ internal void ProjectOperations_EndBuild(object sender, BuildEventArgs args) } var projectPath = _projectService.GetProjectPath(projectItem); - var project = _projectManager.GetLoadedProject(projectPath); - if (project != null && project.WorkspaceProject != null) + var projectSnapshot = _projectManager.GetLoadedProject(projectPath); + if (projectSnapshot != null) { - var workspaceProject = _projectManager.Workspace.CurrentSolution.GetProject(project.WorkspaceProject.Id); + var workspaceProject = _projectManager.Workspace.CurrentSolution?.Projects.FirstOrDefault( + project => FilePathComparer.Instance.Equals(project.FilePath, projectSnapshot.FilePath)); if (workspaceProject != null) { // Trigger a tag helper update by forcing the project manager to see the workspace Project // from the current solution. - _projectManager.WorkspaceProjectChanged(workspaceProject); + _workspaceStateGenerator.Update(workspaceProject, projectSnapshot); } } } diff --git a/src/Razor/src/Microsoft.VisualStudio.Mac.LanguageServices.Razor/ProjectSystem/RazorProjectHostBase.cs b/src/Razor/src/Microsoft.VisualStudio.Mac.LanguageServices.Razor/ProjectSystem/RazorProjectHostBase.cs index c6afacad933..620c2e4cf4d 100644 --- a/src/Razor/src/Microsoft.VisualStudio.Mac.LanguageServices.Razor/ProjectSystem/RazorProjectHostBase.cs +++ b/src/Razor/src/Microsoft.VisualStudio.Mac.LanguageServices.Razor/ProjectSystem/RazorProjectHostBase.cs @@ -152,15 +152,15 @@ private void UpdateHostProjectForeground(object state) } else if (_currentHostProject == null && newHostProject != null) { - _projectSnapshotManager.HostProjectAdded(newHostProject); + _projectSnapshotManager.ProjectAdded(newHostProject); } else if (_currentHostProject != null && newHostProject == null) { - _projectSnapshotManager.HostProjectRemoved(HostProject); + _projectSnapshotManager.ProjectRemoved(HostProject); } else { - _projectSnapshotManager.HostProjectChanged(newHostProject); + _projectSnapshotManager.ProjectConfigurationChanged(newHostProject); } _currentHostProject = newHostProject; diff --git a/src/Razor/src/RazorDeveloperTools/DocumentInfo/RazorDocumentInfoViewModel.cs b/src/Razor/src/RazorDeveloperTools/DocumentInfo/RazorDocumentInfoViewModel.cs index 0a8aa760c64..def6be24b33 100644 --- a/src/Razor/src/RazorDeveloperTools/DocumentInfo/RazorDocumentInfoViewModel.cs +++ b/src/Razor/src/RazorDeveloperTools/DocumentInfo/RazorDocumentInfoViewModel.cs @@ -25,21 +25,6 @@ public RazorDocumentInfoViewModel(VisualStudioDocumentTracker documentTracker) public bool IsSupportedDocument => _documentTracker.IsSupportedProject; - public Project Project - { - get - { - if (Workspace != null && ProjectId != null) - { - return Workspace.CurrentSolution.GetProject(ProjectId); - } - - return null; - } - } - - public ProjectId ProjectId => _documentTracker.Project?.Id; - public Workspace Workspace => _documentTracker.Workspace; } } \ No newline at end of file diff --git a/src/Razor/src/RazorDeveloperTools/RazorInfo/ProjectPropertyCollectionViewModel.cs b/src/Razor/src/RazorDeveloperTools/RazorInfo/ProjectPropertyCollectionViewModel.cs index 1a82786ae4f..094797fb3a0 100644 --- a/src/Razor/src/RazorDeveloperTools/RazorInfo/ProjectPropertyCollectionViewModel.cs +++ b/src/Razor/src/RazorDeveloperTools/RazorInfo/ProjectPropertyCollectionViewModel.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.Collections.ObjectModel; +using System.IO; using System.Linq; using Microsoft.CodeAnalysis.Razor.ProjectSystem; @@ -19,7 +20,7 @@ internal ProjectPropertyCollectionViewModel(ProjectSnapshot project) Properties.Add(new ProjectPropertyItemViewModel("Language Version", _project.Configuration?.LanguageVersion.ToString())); Properties.Add(new ProjectPropertyItemViewModel("Configuration", FormatConfiguration(_project))); Properties.Add(new ProjectPropertyItemViewModel("Extensions", FormatExtensions(_project))); - Properties.Add(new ProjectPropertyItemViewModel("Workspace Project", _project.WorkspaceProject?.Name)); + Properties.Add(new ProjectPropertyItemViewModel("Project", Path.GetFileName(_project.FilePath))); } public ObservableCollection Properties { get; } diff --git a/src/Razor/src/RazorDeveloperTools/RazorInfo/RazorInfoToolWindow.cs b/src/Razor/src/RazorDeveloperTools/RazorInfo/RazorInfoToolWindow.cs index 17efc74d3be..b4196c4d9e5 100644 --- a/src/Razor/src/RazorDeveloperTools/RazorInfo/RazorInfoToolWindow.cs +++ b/src/Razor/src/RazorDeveloperTools/RazorInfo/RazorInfoToolWindow.cs @@ -37,11 +37,12 @@ protected override void Initialize() var componentModel = (IComponentModel)GetService(typeof(SComponentModel)); _workspace = componentModel.GetService(); + var workspaceStateGenerator = componentModel.GetService(); _projectManager = _workspace.Services.GetLanguageServices(RazorLanguage.Name).GetRequiredService(); _projectManager.Changed += ProjectManager_Changed; - DataContext = new RazorInfoViewModel(_workspace, _projectManager, OnException); + DataContext = new RazorInfoViewModel(_workspace, _projectManager, workspaceStateGenerator, OnException); foreach (var project in _projectManager.Projects) { diff --git a/src/Razor/src/RazorDeveloperTools/RazorInfo/RazorInfoViewModel.cs b/src/Razor/src/RazorDeveloperTools/RazorInfo/RazorInfoViewModel.cs index a846e9ff97e..b6d7aa6c27c 100644 --- a/src/Razor/src/RazorDeveloperTools/RazorInfo/RazorInfoViewModel.cs +++ b/src/Razor/src/RazorDeveloperTools/RazorInfo/RazorInfoViewModel.cs @@ -3,8 +3,10 @@ using System; using System.Collections.ObjectModel; +using System.Linq; using System.Windows.Input; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Razor; using Microsoft.CodeAnalysis.Razor.ProjectSystem; namespace Microsoft.VisualStudio.RazorExtension.RazorInfo @@ -13,6 +15,7 @@ internal class RazorInfoViewModel : NotifyPropertyChanged { private readonly Workspace _workspace; private readonly ProjectSnapshotManager _projectManager; + private readonly ProjectWorkspaceStateGenerator _workspaceStateGenerator; private readonly Action _errorHandler; private ProjectViewModel _selectedProject; @@ -22,10 +25,15 @@ internal class RazorInfoViewModel : NotifyPropertyChanged private TagHelperCollectionViewModel _tagHelpers; private ICommand _updateCommand; - public RazorInfoViewModel(Workspace workspace, ProjectSnapshotManager projectManager, Action errorHandler) + public RazorInfoViewModel( + Workspace workspace, + ProjectSnapshotManager projectManager, + ProjectWorkspaceStateGenerator workspaceStateGenerator, + Action errorHandler) { _workspace = workspace; _projectManager = projectManager; + _workspaceStateGenerator = workspaceStateGenerator; _errorHandler = errorHandler; UpdateCommand = new RelayCommand(ExecuteUpdate, CanExecuteUpdate); @@ -190,14 +198,15 @@ private void ExecuteUpdate(object state) return; } - var project = _projectManager.GetLoadedProject(projectFilePath); - if (project != null && project.WorkspaceProject != null) + var projectSnapshot = _projectManager.GetLoadedProject(projectFilePath); + if (projectSnapshot != null) { + var workspaceProject = _workspace.CurrentSolution.Projects.FirstOrDefault( + wp => FilePathComparer.Instance.Equals(wp.FilePath, SelectedProject.FilePath)); var solution = _workspace.CurrentSolution; - var workspaceProject = solution.GetProject(project.WorkspaceProject.Id); if (workspaceProject != null) { - ((ProjectSnapshotManagerBase)_projectManager).WorkspaceProjectChanged(workspaceProject); + _workspaceStateGenerator.Update(workspaceProject, projectSnapshot); } } } diff --git a/src/Razor/src/RazorDeveloperTools/RazorInfo/TagHelperCollectionViewModel.cs b/src/Razor/src/RazorDeveloperTools/RazorInfo/TagHelperCollectionViewModel.cs index 9a3766dc92a..8d3f2cd9a5c 100644 --- a/src/Razor/src/RazorDeveloperTools/RazorInfo/TagHelperCollectionViewModel.cs +++ b/src/Razor/src/RazorDeveloperTools/RazorInfo/TagHelperCollectionViewModel.cs @@ -43,12 +43,9 @@ private async void InitializeTagHelpers() try { - if (!_project.TryGetTagHelpers(out var tagHelpers)) - { - ProgressVisibility = Visibility.Visible; - tagHelpers = await _project.GetTagHelpersAsync(); - await Task.Delay(250); // Force a delay for the UI - } + var tagHelpers = _project.TagHelpers; + ProgressVisibility = Visibility.Visible; + await Task.Delay(250); // Force a delay for the UI foreach (var tagHelper in tagHelpers) { diff --git a/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/ProjectSystem/DefaultProjectSnapshotTest.cs b/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/ProjectSystem/DefaultProjectSnapshotTest.cs index fa6c1f0aed2..5f64830f2b5 100644 --- a/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/ProjectSystem/DefaultProjectSnapshotTest.cs +++ b/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/ProjectSystem/DefaultProjectSnapshotTest.cs @@ -18,16 +18,10 @@ public DefaultProjectSnapshotTest() HostProject = new HostProject(TestProjectData.SomeProject.FilePath, FallbackRazorConfiguration.MVC_2_0); HostProjectWithConfigurationChange = new HostProject(TestProjectData.SomeProject.FilePath, FallbackRazorConfiguration.MVC_1_0); - - var projectId = ProjectId.CreateNewId("Test"); - var solution = Workspace.CurrentSolution.AddProject(ProjectInfo.Create( - projectId, - VersionStamp.Default, - "Test", - "Test", - LanguageNames.CSharp, - TestProjectData.SomeProject.FilePath)); - WorkspaceProject = solution.GetProject(projectId); + ProjectWorkspaceState = new ProjectWorkspaceState(new[] + { + TagHelperDescriptorBuilder.Create("TestTagHelper", "TestAssembly").Build(), + }); SomeTagHelpers = new List { @@ -50,7 +44,7 @@ public DefaultProjectSnapshotTest() private HostProject HostProjectWithConfigurationChange { get; } - private Project WorkspaceProject { get; } + private ProjectWorkspaceState ProjectWorkspaceState { get; } private TestTagHelperResolver TagHelperResolver { get; } @@ -70,7 +64,7 @@ protected override void ConfigureProjectEngine(RazorProjectEngineBuilder builder public void ProjectSnapshot_CachesDocumentSnapshots() { // Arrange - var state = ProjectState.Create(Workspace.Services, HostProject, WorkspaceProject) + var state = ProjectState.Create(Workspace.Services, HostProject, ProjectWorkspaceState) .WithAddedHostDocument(Documents[0], DocumentState.EmptyLoader) .WithAddedHostDocument(Documents[1], DocumentState.EmptyLoader) .WithAddedHostDocument(Documents[2], DocumentState.EmptyLoader); @@ -91,7 +85,7 @@ public void ProjectSnapshot_CachesDocumentSnapshots() public void IsImportDocument_NonImportDocument_ReturnsFalse() { // Arrange - var state = ProjectState.Create(Workspace.Services, HostProject, WorkspaceProject) + var state = ProjectState.Create(Workspace.Services, HostProject, ProjectWorkspaceState) .WithAddedHostDocument(Documents[0], DocumentState.EmptyLoader); var snapshot = new DefaultProjectSnapshot(state); @@ -108,7 +102,7 @@ public void IsImportDocument_NonImportDocument_ReturnsFalse() public void IsImportDocument_ImportDocument_ReturnsTrue() { // Arrange - var state = ProjectState.Create(Workspace.Services, HostProject, WorkspaceProject) + var state = ProjectState.Create(Workspace.Services, HostProject, ProjectWorkspaceState) .WithAddedHostDocument(Documents[0], DocumentState.EmptyLoader) .WithAddedHostDocument(TestProjectData.SomeProjectImportFile, DocumentState.EmptyLoader); var snapshot = new DefaultProjectSnapshot(state); @@ -126,7 +120,7 @@ public void IsImportDocument_ImportDocument_ReturnsTrue() public void GetRelatedDocuments_NonImportDocument_ReturnsEmpty() { // Arrange - var state = ProjectState.Create(Workspace.Services, HostProject, WorkspaceProject) + var state = ProjectState.Create(Workspace.Services, HostProject, ProjectWorkspaceState) .WithAddedHostDocument(Documents[0], DocumentState.EmptyLoader); var snapshot = new DefaultProjectSnapshot(state); @@ -143,7 +137,7 @@ public void GetRelatedDocuments_NonImportDocument_ReturnsEmpty() public void GetRelatedDocuments_ImportDocument_ReturnsRelated() { // Arrange - var state = ProjectState.Create(Workspace.Services, HostProject, WorkspaceProject) + var state = ProjectState.Create(Workspace.Services, HostProject, ProjectWorkspaceState) .WithAddedHostDocument(Documents[0], DocumentState.EmptyLoader) .WithAddedHostDocument(Documents[1], DocumentState.EmptyLoader) .WithAddedHostDocument(TestProjectData.SomeProjectImportFile, DocumentState.EmptyLoader); diff --git a/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/ProjectSystem/DocumentStateTest.cs b/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/ProjectSystem/DocumentStateTest.cs index 58e64d287c4..ba9adec490c 100644 --- a/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/ProjectSystem/DocumentStateTest.cs +++ b/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/ProjectSystem/DocumentStateTest.cs @@ -19,16 +19,10 @@ public DocumentStateTest() HostProject = new HostProject(TestProjectData.SomeProject.FilePath, FallbackRazorConfiguration.MVC_2_0); HostProjectWithConfigurationChange = new HostProject(TestProjectData.SomeProject.FilePath, FallbackRazorConfiguration.MVC_1_0); - - var projectId = ProjectId.CreateNewId("Test"); - var solution = Workspace.CurrentSolution.AddProject(ProjectInfo.Create( - projectId, - VersionStamp.Default, - "Test", - "Test", - LanguageNames.CSharp, - TestProjectData.SomeProject.FilePath)); - WorkspaceProject = solution.GetProject(projectId); + ProjectWorkspaceState = new ProjectWorkspaceState(new[] + { + TagHelperDescriptorBuilder.Create("TestTagHelper", "TestAssembly").Build(), + }); SomeTagHelpers = new List(); SomeTagHelpers.Add(TagHelperDescriptorBuilder.Create("Test1", "TestAssembly").Build()); @@ -45,7 +39,7 @@ public DocumentStateTest() private HostProject HostProjectWithConfigurationChange { get; } - private Project WorkspaceProject { get; } + private ProjectWorkspaceState ProjectWorkspaceState { get; } private TestTagHelperResolver TagHelperResolver { get; } @@ -164,14 +158,14 @@ public async Task DocumentState_WithImportsChange_CachesLoadedText() } [Fact] - public void DocumentState_WithWorkspaceProjectChange_CachesSnapshotText() + public void DocumentState_WithProjectWorkspaceStateChange_CachesSnapshotText() { // Arrange var original = DocumentState.Create(Workspace.Services, HostDocument, DocumentState.EmptyLoader) .WithText(Text, VersionStamp.Create()); // Act - var state = original.WithWorkspaceProjectChange(); + var state = original.WithProjectWorkspaceStateChange(); // Assert Assert.True(state.TryGetText(out _)); @@ -179,7 +173,7 @@ public void DocumentState_WithWorkspaceProjectChange_CachesSnapshotText() } [Fact] - public async Task DocumentState_WithWorkspaceProjectChange_CachesLoadedText() + public async Task DocumentState_WithProjectWorkspaceStateChange_CachesLoadedText() { // Arrange var original = DocumentState.Create(Workspace.Services, HostDocument, DocumentState.EmptyLoader) @@ -188,7 +182,7 @@ public async Task DocumentState_WithWorkspaceProjectChange_CachesLoadedText() await original.GetTextAsync(); // Act - var state = original.WithWorkspaceProjectChange(); + var state = original.WithProjectWorkspaceStateChange(); // Assert Assert.True(state.TryGetText(out _)); diff --git a/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/ProjectSystem/ProjectStateGeneratedOutputTest.cs b/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/ProjectSystem/ProjectStateGeneratedOutputTest.cs index fd9691517d0..9b8940fdf43 100644 --- a/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/ProjectSystem/ProjectStateGeneratedOutputTest.cs +++ b/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/ProjectSystem/ProjectStateGeneratedOutputTest.cs @@ -19,16 +19,6 @@ public ProjectStateGeneratedOutputTest() HostProject = new HostProject(TestProjectData.SomeProject.FilePath, FallbackRazorConfiguration.MVC_2_0); HostProjectWithConfigurationChange = new HostProject(TestProjectData.SomeProject.FilePath, FallbackRazorConfiguration.MVC_1_0); - var projectId = ProjectId.CreateNewId("Test"); - var solution = Workspace.CurrentSolution.AddProject(ProjectInfo.Create( - projectId, - VersionStamp.Default, - "Test", - "Test", - LanguageNames.CSharp, - TestProjectData.SomeProject.FilePath)); - WorkspaceProject = solution.GetProject(projectId); - SomeTagHelpers = new List(); SomeTagHelpers.Add(TagHelperDescriptorBuilder.Create("Test1", "TestAssembly").Build()); @@ -44,8 +34,6 @@ public ProjectStateGeneratedOutputTest() private HostProject HostProjectWithConfigurationChange { get; } - private Project WorkspaceProject { get; } - private TestTagHelperResolver TagHelperResolver { get; } = new TestTagHelperResolver(); private List SomeTagHelpers { get; } @@ -82,7 +70,8 @@ public async Task HostDocumentAdded_CachesOutput() Assert.Same(originalOutput, actualOutput); Assert.Equal(originalInputVersion, actualInputVersion); Assert.Equal(originalOutputVersion, actualOutputVersion); - Assert.Equal(await state.GetComputedStateVersionAsync(new DefaultProjectSnapshot(state)), actualOutputVersion); + Assert.NotEqual(state.ProjectWorkspaceStateVersion, actualOutputVersion); + Assert.NotEqual(state.ConfigurationVersion, actualOutputVersion); } [Fact] @@ -181,29 +170,31 @@ public async Task HostDocumentRemoved_Import_DoesNotCacheOutput() } [Fact] - public async Task WorkspaceProjectChange_CachesOutput() + public async Task ProjectWorkspaceStateChange_CachesOutput_EvenWhenNewerProjectWorkspaceState() { // Arrange var original = ProjectState.Create(Workspace.Services, HostProject) - .WithAddedHostDocument(HostDocument, DocumentState.EmptyLoader); + .WithAddedHostDocument(HostDocument, DocumentState.EmptyLoader) + .WithProjectWorkspaceState(ProjectWorkspaceState.Default); var (originalOutput, originalInputVersion, originalOutputVersion) = await GetOutputAsync(original, HostDocument); + var changed = new ProjectWorkspaceState(Array.Empty()); // Act - var state = original.WithWorkspaceProject(WorkspaceProject.WithAssemblyName("Test2")); + var state = original.WithProjectWorkspaceState(changed); // Assert var (actualOutput, actualInputVersion, actualOutputVersion) = await GetOutputAsync(state, HostDocument); Assert.Same(originalOutput, actualOutput); Assert.Equal(originalInputVersion, actualInputVersion); Assert.Equal(originalOutputVersion, actualOutputVersion); - Assert.Equal(await state.GetComputedStateVersionAsync(new DefaultProjectSnapshot(state)), actualInputVersion); + Assert.Equal(state.ProjectWorkspaceStateVersion, actualInputVersion); } // The generated code's text doesn't change as a result, so the output version does not change [Fact] - public async Task WorkspaceProjectChange_WithTagHelperChange_DoesNotCacheOutput() + public async Task ProjectWorkspaceStateChange_WithTagHelperChange_DoesNotCacheOutput() { // Arrange var original = @@ -211,18 +202,17 @@ public async Task WorkspaceProjectChange_WithTagHelperChange_DoesNotCacheOutput( .WithAddedHostDocument(HostDocument, DocumentState.EmptyLoader); var (originalOutput, originalInputVersion, originalOutputVersion) = await GetOutputAsync(original, HostDocument); - - TagHelperResolver.TagHelpers = SomeTagHelpers; + var changed = new ProjectWorkspaceState(SomeTagHelpers); // Act - var state = original.WithWorkspaceProject(WorkspaceProject.WithAssemblyName("Test2")); + var state = original.WithProjectWorkspaceState(changed); // Assert var (actualOutput, actualInputVersion, actualOutputVersion) = await GetOutputAsync(state, HostDocument); Assert.NotSame(originalOutput, actualOutput); Assert.NotEqual(originalInputVersion, actualInputVersion); Assert.Equal(originalOutputVersion, actualOutputVersion); - Assert.Equal(await state.GetComputedStateVersionAsync(new DefaultProjectSnapshot(state)), actualInputVersion); + Assert.Equal(state.ProjectWorkspaceStateVersion, actualInputVersion); } [Fact] @@ -243,7 +233,7 @@ public async Task ConfigurationChange_DoesNotCacheOutput() Assert.NotSame(originalOutput, actualOutput); Assert.NotEqual(originalInputVersion, actualInputVersion); Assert.NotEqual(originalOutputVersion, actualOutputVersion); - Assert.Equal(await state.GetComputedStateVersionAsync(new DefaultProjectSnapshot(state)), actualInputVersion); + Assert.NotEqual(state.ProjectWorkspaceStateVersion, actualInputVersion); } private static Task<(RazorCodeDocument, VersionStamp, VersionStamp)> GetOutputAsync(ProjectState project, HostDocument hostDocument) diff --git a/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/ProjectSystem/ProjectStateTest.cs b/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/ProjectSystem/ProjectStateTest.cs index c5d6c276724..4ca1dde8588 100644 --- a/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/ProjectSystem/ProjectStateTest.cs +++ b/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/ProjectSystem/ProjectStateTest.cs @@ -9,6 +9,7 @@ using Microsoft.AspNetCore.Razor.Language; using Microsoft.CodeAnalysis.Host; using Microsoft.CodeAnalysis.Text; +using Moq; using Xunit; namespace Microsoft.CodeAnalysis.Razor.ProjectSystem @@ -19,16 +20,10 @@ public ProjectStateTest() { HostProject = new HostProject(TestProjectData.SomeProject.FilePath, FallbackRazorConfiguration.MVC_2_0); HostProjectWithConfigurationChange = new HostProject(TestProjectData.SomeProject.FilePath, FallbackRazorConfiguration.MVC_1_0); - - var projectId = ProjectId.CreateNewId("Test"); - var solution = Workspace.CurrentSolution.AddProject(ProjectInfo.Create( - projectId, - VersionStamp.Default, - "Test", - "Test", - LanguageNames.CSharp, - TestProjectData.SomeProject.FilePath)); - WorkspaceProject = solution.GetProject(projectId); + ProjectWorkspaceState = new ProjectWorkspaceState(new[] + { + TagHelperDescriptorBuilder.Create("TestTagHelper", "TestAssembly").Build(), + }); SomeTagHelpers = new List(); SomeTagHelpers.Add(TagHelperDescriptorBuilder.Create("Test1", "TestAssembly").Build()); @@ -52,7 +47,7 @@ public ProjectStateTest() private HostProject HostProjectWithConfigurationChange { get; } - private Project WorkspaceProject { get; } + private ProjectWorkspaceState ProjectWorkspaceState { get; } private TestTagHelperResolver TagHelperResolver { get; set; } @@ -77,9 +72,9 @@ protected override void ConfigureProjectEngine(RazorProjectEngineBuilder builder public void ProjectState_ConstructedNew() { // Arrange - + // Act - var state = ProjectState.Create(Workspace.Services, HostProject, WorkspaceProject); + var state = ProjectState.Create(Workspace.Services, HostProject, ProjectWorkspaceState); // Assert Assert.Empty(state.Documents); @@ -90,7 +85,7 @@ public void ProjectState_ConstructedNew() public void ProjectState_AddHostDocument_ToEmpty() { // Arrange - var original = ProjectState.Create(Workspace.Services, HostProject, WorkspaceProject); + var original = ProjectState.Create(Workspace.Services, HostProject, ProjectWorkspaceState); // Act var state = original.WithAddedHostDocument(Documents[0], DocumentState.EmptyLoader); @@ -108,7 +103,7 @@ public void ProjectState_AddHostDocument_ToEmpty() public async Task ProjectState_AddHostDocument_DocumentIsEmpty() { // Arrange - var original = ProjectState.Create(Workspace.Services, HostProject, WorkspaceProject); + var original = ProjectState.Create(Workspace.Services, HostProject, ProjectWorkspaceState); // Act var state = original.WithAddedHostDocument(Documents[0], DocumentState.EmptyLoader); @@ -122,7 +117,7 @@ public async Task ProjectState_AddHostDocument_DocumentIsEmpty() public void ProjectState_AddHostDocument_ToProjectWithDocuments() { // Arrange - var original = ProjectState.Create(Workspace.Services, HostProject, WorkspaceProject) + var original = ProjectState.Create(Workspace.Services, HostProject, ProjectWorkspaceState) .WithAddedHostDocument(Documents[2], DocumentState.EmptyLoader) .WithAddedHostDocument(Documents[1], DocumentState.EmptyLoader); @@ -144,14 +139,14 @@ public void ProjectState_AddHostDocument_ToProjectWithDocuments() public void ProjectState_AddHostDocument_TracksImports() { // Arrange - + // Act - var state = ProjectState.Create(Workspace.Services, HostProject, WorkspaceProject) + var state = ProjectState.Create(Workspace.Services, HostProject, ProjectWorkspaceState) .WithAddedHostDocument(TestProjectData.SomeProjectFile1, DocumentState.EmptyLoader) .WithAddedHostDocument(TestProjectData.SomeProjectFile2, DocumentState.EmptyLoader) .WithAddedHostDocument(TestProjectData.SomeProjectNestedFile3, DocumentState.EmptyLoader) .WithAddedHostDocument(TestProjectData.AnotherProjectNestedFile4, DocumentState.EmptyLoader); - + // Assert Assert.Collection( state.ImportsToRelatedDocuments.OrderBy(kvp => kvp.Key), @@ -185,7 +180,7 @@ public void ProjectState_AddHostDocument_TracksImports() public void ProjectState_AddHostDocument_TracksImports_AddImportFile() { // Arrange - var original = ProjectState.Create(Workspace.Services, HostProject, WorkspaceProject) + var original = ProjectState.Create(Workspace.Services, HostProject, ProjectWorkspaceState) .WithAddedHostDocument(TestProjectData.SomeProjectFile1, DocumentState.EmptyLoader) .WithAddedHostDocument(TestProjectData.SomeProjectFile2, DocumentState.EmptyLoader) .WithAddedHostDocument(TestProjectData.SomeProjectNestedFile3, DocumentState.EmptyLoader) @@ -225,27 +220,27 @@ public void ProjectState_AddHostDocument_TracksImports_AddImportFile() } [Fact] - public async Task ProjectState_AddHostDocument_RetainsComputedState() + public void ProjectState_AddHostDocument_RetainsComputedState() { // Arrange - var original = ProjectState.Create(Workspace.Services, HostProject, WorkspaceProject) + var original = ProjectState.Create(Workspace.Services, HostProject, ProjectWorkspaceState) .WithAddedHostDocument(Documents[2], DocumentState.EmptyLoader) .WithAddedHostDocument(Documents[1], DocumentState.EmptyLoader); // Force init - var originalTagHelpers = await original.GetTagHelpersAsync(new DefaultProjectSnapshot(original)); - var originalComputedVersion = await original.GetComputedStateVersionAsync(new DefaultProjectSnapshot(original)); + var originalTagHelpers = original.TagHelpers; + var originalProjectWorkspaceStateVersion = original.ProjectWorkspaceStateVersion; // Act var state = original.WithAddedHostDocument(Documents[0], DocumentState.EmptyLoader); // Assert - var actualTagHelpers = await state.GetTagHelpersAsync(new DefaultProjectSnapshot(state)); - var actualComputedVersion = await state.GetComputedStateVersionAsync(new DefaultProjectSnapshot(state)); + var actualTagHelpers = state.TagHelpers; + var actualProjectWorkspaceStateVersion = state.ProjectWorkspaceStateVersion; Assert.Same(original.ProjectEngine, state.ProjectEngine); Assert.Same(originalTagHelpers, actualTagHelpers); - Assert.Equal(originalComputedVersion, actualComputedVersion); + Assert.Equal(originalProjectWorkspaceStateVersion, actualProjectWorkspaceStateVersion); Assert.Same(original.Documents[Documents[1].FilePath], state.Documents[Documents[1].FilePath]); Assert.Same(original.Documents[Documents[2].FilePath], state.Documents[Documents[2].FilePath]); @@ -255,7 +250,7 @@ public async Task ProjectState_AddHostDocument_RetainsComputedState() public void ProjectState_AddHostDocument_DuplicateNoops() { // Arrange - var original = ProjectState.Create(Workspace.Services, HostProject, WorkspaceProject) + var original = ProjectState.Create(Workspace.Services, HostProject, ProjectWorkspaceState) .WithAddedHostDocument(Documents[2], DocumentState.EmptyLoader) .WithAddedHostDocument(Documents[1], DocumentState.EmptyLoader); @@ -270,7 +265,7 @@ public void ProjectState_AddHostDocument_DuplicateNoops() public async Task ProjectState_WithChangedHostDocument_Loader() { // Arrange - var original = ProjectState.Create(Workspace.Services, HostProject, WorkspaceProject) + var original = ProjectState.Create(Workspace.Services, HostProject, ProjectWorkspaceState) .WithAddedHostDocument(Documents[2], DocumentState.EmptyLoader) .WithAddedHostDocument(Documents[1], DocumentState.EmptyLoader); @@ -290,7 +285,7 @@ public async Task ProjectState_WithChangedHostDocument_Loader() public async Task ProjectState_WithChangedHostDocument_Snapshot() { // Arrange - var original = ProjectState.Create(Workspace.Services, HostProject, WorkspaceProject) + var original = ProjectState.Create(Workspace.Services, HostProject, ProjectWorkspaceState) .WithAddedHostDocument(Documents[2], DocumentState.EmptyLoader) .WithAddedHostDocument(Documents[1], DocumentState.EmptyLoader); @@ -307,53 +302,53 @@ public async Task ProjectState_WithChangedHostDocument_Snapshot() } [Fact] - public async Task ProjectState_WithChangedHostDocument_Loader_RetainsComputedState() + public void ProjectState_WithChangedHostDocument_Loader_RetainsComputedState() { // Arrange - var original = ProjectState.Create(Workspace.Services, HostProject, WorkspaceProject) + var original = ProjectState.Create(Workspace.Services, HostProject, ProjectWorkspaceState) .WithAddedHostDocument(Documents[2], DocumentState.EmptyLoader) .WithAddedHostDocument(Documents[1], DocumentState.EmptyLoader); // Force init - var originalTagHelpers = await original.GetTagHelpersAsync(new DefaultProjectSnapshot(original)); - var originalComputedVersion = await original.GetComputedStateVersionAsync(new DefaultProjectSnapshot(original)); + var originalTagHelpers = original.TagHelpers; + var originalProjectWorkspaceStateVersion = original.ProjectWorkspaceStateVersion; // Act var state = original.WithChangedHostDocument(Documents[1], TextLoader); // Assert - var actualTagHelpers = await state.GetTagHelpersAsync(new DefaultProjectSnapshot(state)); - var actualComputedVersion = await state.GetComputedStateVersionAsync(new DefaultProjectSnapshot(state)); + var actualTagHelpers = state.TagHelpers; + var actualProjectWorkspaceStateVersion = state.ProjectWorkspaceStateVersion; Assert.Same(original.ProjectEngine, state.ProjectEngine); Assert.Same(originalTagHelpers, actualTagHelpers); - Assert.Equal(originalComputedVersion, actualComputedVersion); + Assert.Equal(originalProjectWorkspaceStateVersion, actualProjectWorkspaceStateVersion); Assert.NotSame(original.Documents[Documents[1].FilePath], state.Documents[Documents[1].FilePath]); } [Fact] - public async Task ProjectState_WithChangedHostDocument_Snapshot_RetainsComputedState() + public void ProjectState_WithChangedHostDocument_Snapshot_RetainsComputedState() { // Arrange - var original = ProjectState.Create(Workspace.Services, HostProject, WorkspaceProject) + var original = ProjectState.Create(Workspace.Services, HostProject, ProjectWorkspaceState) .WithAddedHostDocument(Documents[2], DocumentState.EmptyLoader) .WithAddedHostDocument(Documents[1], DocumentState.EmptyLoader); // Force init - var originalTagHelpers = await original.GetTagHelpersAsync(new DefaultProjectSnapshot(original)); - var originalComputedVersion = await original.GetComputedStateVersionAsync(new DefaultProjectSnapshot(original)); + var originalTagHelpers = original.TagHelpers; + var originalProjectWorkspaceStateVersion = original.ProjectWorkspaceStateVersion; // Act var state = original.WithChangedHostDocument(Documents[1], Text, VersionStamp.Create()); // Assert - var actualTagHelpers = await state.GetTagHelpersAsync(new DefaultProjectSnapshot(state)); - var actualComputedVersion = await state.GetComputedStateVersionAsync(new DefaultProjectSnapshot(state)); + var actualTagHelpers = state.TagHelpers; + var actualProjectWorkspaceStateVersion = state.ProjectWorkspaceStateVersion; Assert.Same(original.ProjectEngine, state.ProjectEngine); Assert.Same(originalTagHelpers, actualTagHelpers); - Assert.Equal(originalComputedVersion, actualComputedVersion); + Assert.Equal(originalProjectWorkspaceStateVersion, actualProjectWorkspaceStateVersion); Assert.NotSame(original.Documents[Documents[1].FilePath], state.Documents[Documents[1].FilePath]); } @@ -362,7 +357,7 @@ public async Task ProjectState_WithChangedHostDocument_Snapshot_RetainsComputedS public void ProjectState_WithChangedHostDocument_Loader_NotFoundNoops() { // Arrange - var original = ProjectState.Create(Workspace.Services, HostProject, WorkspaceProject) + var original = ProjectState.Create(Workspace.Services, HostProject, ProjectWorkspaceState) .WithAddedHostDocument(Documents[2], DocumentState.EmptyLoader) .WithAddedHostDocument(Documents[1], DocumentState.EmptyLoader); @@ -377,7 +372,7 @@ public void ProjectState_WithChangedHostDocument_Loader_NotFoundNoops() public void ProjectState_WithChangedHostDocument_Snapshot_NotFoundNoops() { // Arrange - var original = ProjectState.Create(Workspace.Services, HostProject, WorkspaceProject) + var original = ProjectState.Create(Workspace.Services, HostProject, ProjectWorkspaceState) .WithAddedHostDocument(Documents[2], DocumentState.EmptyLoader) .WithAddedHostDocument(Documents[1], DocumentState.EmptyLoader); @@ -392,7 +387,7 @@ public void ProjectState_WithChangedHostDocument_Snapshot_NotFoundNoops() public void ProjectState_RemoveHostDocument_FromProjectWithDocuments() { // Arrange - var original = ProjectState.Create(Workspace.Services, HostProject, WorkspaceProject) + var original = ProjectState.Create(Workspace.Services, HostProject, ProjectWorkspaceState) .WithAddedHostDocument(Documents[2], DocumentState.EmptyLoader) .WithAddedHostDocument(Documents[1], DocumentState.EmptyLoader); @@ -413,7 +408,7 @@ public void ProjectState_RemoveHostDocument_FromProjectWithDocuments() public void ProjectState_RemoveHostDocument_TracksImports() { // Arrange - var original = ProjectState.Create(Workspace.Services, HostProject, WorkspaceProject) + var original = ProjectState.Create(Workspace.Services, HostProject, ProjectWorkspaceState) .WithAddedHostDocument(TestProjectData.SomeProjectFile1, DocumentState.EmptyLoader) .WithAddedHostDocument(TestProjectData.SomeProjectFile2, DocumentState.EmptyLoader) .WithAddedHostDocument(TestProjectData.SomeProjectNestedFile3, DocumentState.EmptyLoader) @@ -453,7 +448,7 @@ public void ProjectState_RemoveHostDocument_TracksImports() public void ProjectState_RemoveHostDocument_TracksImports_RemoveAllDocuments() { // Arrange - var original = ProjectState.Create(Workspace.Services, HostProject, WorkspaceProject) + var original = ProjectState.Create(Workspace.Services, HostProject, ProjectWorkspaceState) .WithAddedHostDocument(TestProjectData.SomeProjectFile1, DocumentState.EmptyLoader) .WithAddedHostDocument(TestProjectData.SomeProjectFile2, DocumentState.EmptyLoader) .WithAddedHostDocument(TestProjectData.SomeProjectNestedFile3, DocumentState.EmptyLoader) @@ -472,27 +467,27 @@ public void ProjectState_RemoveHostDocument_TracksImports_RemoveAllDocuments() } [Fact] - public async Task ProjectState_RemoveHostDocument_RetainsComputedState() + public void ProjectState_RemoveHostDocument_RetainsComputedState() { // Arrange - var original = ProjectState.Create(Workspace.Services, HostProject, WorkspaceProject) + var original = ProjectState.Create(Workspace.Services, HostProject, ProjectWorkspaceState) .WithAddedHostDocument(Documents[2], DocumentState.EmptyLoader) .WithAddedHostDocument(Documents[1], DocumentState.EmptyLoader); // Force init - var originalTagHelpers = await original.GetTagHelpersAsync(new DefaultProjectSnapshot(original)); - var originalComputedVersion = await original.GetComputedStateVersionAsync(new DefaultProjectSnapshot(original)); + var originalTagHelpers = original.TagHelpers; + var originalProjectWorkspaceStateVersion = original.ProjectWorkspaceStateVersion; // Act var state = original.WithRemovedHostDocument(Documents[2]); // Assert - var actualTagHelpers = await state.GetTagHelpersAsync(new DefaultProjectSnapshot(state)); - var actualComputedVersion = await state.GetComputedStateVersionAsync(new DefaultProjectSnapshot(state)); + var actualTagHelpers = state.TagHelpers; + var actualProjectWorkspaceStateVersion = state.ProjectWorkspaceStateVersion; Assert.Same(original.ProjectEngine, state.ProjectEngine); Assert.Same(originalTagHelpers, actualTagHelpers); - Assert.Equal(originalComputedVersion, actualComputedVersion); + Assert.Equal(originalProjectWorkspaceStateVersion, actualProjectWorkspaceStateVersion); Assert.Same(original.Documents[Documents[1].FilePath], state.Documents[Documents[1].FilePath]); } @@ -501,7 +496,7 @@ public async Task ProjectState_RemoveHostDocument_RetainsComputedState() public void ProjectState_RemoveHostDocument_NotFoundNoops() { // Arrange - var original = ProjectState.Create(Workspace.Services, HostProject, WorkspaceProject) + var original = ProjectState.Create(Workspace.Services, HostProject, ProjectWorkspaceState) .WithAddedHostDocument(Documents[2], DocumentState.EmptyLoader) .WithAddedHostDocument(Documents[1], DocumentState.EmptyLoader); @@ -513,16 +508,16 @@ public void ProjectState_RemoveHostDocument_NotFoundNoops() } [Fact] - public async Task ProjectState_WithHostProject_ConfigurationChange_UpdatesComputedState() + public void ProjectState_WithHostProject_ConfigurationChange_UpdatesConfigurationState() { // Arrange - var original = ProjectState.Create(Workspace.Services, HostProject, WorkspaceProject) + var original = ProjectState.Create(Workspace.Services, HostProject, ProjectWorkspaceState) .WithAddedHostDocument(Documents[2], DocumentState.EmptyLoader) .WithAddedHostDocument(Documents[1], DocumentState.EmptyLoader); // Force init - var originalTagHelpers = await original.GetTagHelpersAsync(new DefaultProjectSnapshot(original)); - var originalComputedVersion = await original.GetComputedStateVersionAsync(new DefaultProjectSnapshot(original)); + var originalTagHelpers = original.TagHelpers; + var originalProjectWorkspaceStateVersion = original.ConfigurationVersion; TagHelperResolver.TagHelpers = SomeTagHelpers; @@ -533,12 +528,12 @@ public async Task ProjectState_WithHostProject_ConfigurationChange_UpdatesComput Assert.NotEqual(original.Version, state.Version); Assert.Same(HostProjectWithConfigurationChange, state.HostProject); - var actualTagHelpers = await state.GetTagHelpersAsync(new DefaultProjectSnapshot(state)); - var actualComputedVersion = await state.GetComputedStateVersionAsync(new DefaultProjectSnapshot(state)); + var actualTagHelpers = state.TagHelpers; + var actualProjectWorkspaceStateVersion = state.ConfigurationVersion; Assert.NotSame(original.ProjectEngine, state.ProjectEngine); - Assert.NotSame(originalTagHelpers, actualTagHelpers); - Assert.NotEqual(originalComputedVersion, actualComputedVersion); + Assert.Same(originalTagHelpers, actualTagHelpers); + Assert.NotEqual(originalProjectWorkspaceStateVersion, actualProjectWorkspaceStateVersion); Assert.NotSame(original.Documents[Documents[1].FilePath], state.Documents[Documents[1].FilePath]); Assert.NotSame(original.Documents[Documents[2].FilePath], state.Documents[Documents[2].FilePath]); @@ -547,16 +542,15 @@ public async Task ProjectState_WithHostProject_ConfigurationChange_UpdatesComput } [Fact] - public async Task ProjectState_WithHostProject_NoConfigurationChange_Noops() + public void ProjectState_WithHostProject_NoConfigurationChange_Noops() { // Arrange - var original = ProjectState.Create(Workspace.Services, HostProject, WorkspaceProject) + var original = ProjectState.Create(Workspace.Services, HostProject, ProjectWorkspaceState) .WithAddedHostDocument(Documents[2], DocumentState.EmptyLoader) .WithAddedHostDocument(Documents[1], DocumentState.EmptyLoader); // Force init - var originalTagHelpers = await original.GetTagHelpersAsync(new DefaultProjectSnapshot(original)); - var originalComputedVersion = await original.GetComputedStateVersionAsync(new DefaultProjectSnapshot(original)); + var originalProjectWorkspaceStateVersion = original.ProjectWorkspaceStateVersion; // Act var state = original.WithHostProject(HostProject); @@ -570,16 +564,14 @@ public void ProjectState_WithHostProject_CallsConfigurationChangeOnDocumentState { // Arrange var callCount = 0; - + var documents = ImmutableDictionary.CreateBuilder(FilePathComparer.Instance); documents[Documents[1].FilePath] = TestDocumentState.Create(Workspace.Services, Documents[1], onConfigurationChange: () => callCount++); documents[Documents[2].FilePath] = TestDocumentState.Create(Workspace.Services, Documents[2], onConfigurationChange: () => callCount++); - var original = ProjectState.Create(Workspace.Services, HostProject, WorkspaceProject); + var original = ProjectState.Create(Workspace.Services, HostProject, ProjectWorkspaceState); original.Documents = documents.ToImmutable(); - var changed = WorkspaceProject.WithAssemblyName("Test1"); - // Act var state = original.WithHostProject(HostProjectWithConfigurationChange); @@ -590,38 +582,39 @@ public void ProjectState_WithHostProject_CallsConfigurationChangeOnDocumentState } [Fact] - public async Task ProjectState_WithWorkspaceProject_Removed() + public void ProjectState_WithProjectWorkspaceState_Removed() { // Arrange - var original = ProjectState.Create(Workspace.Services, HostProject, WorkspaceProject) + var emptyProjectWorkspaceState = new ProjectWorkspaceState(Array.Empty()); + var original = ProjectState.Create(Workspace.Services, HostProject, emptyProjectWorkspaceState) .WithAddedHostDocument(Documents[2], DocumentState.EmptyLoader) .WithAddedHostDocument(Documents[1], DocumentState.EmptyLoader); // Force init - var originalTagHelpers = await original.GetTagHelpersAsync(new DefaultProjectSnapshot(original)); - var originalComputedVersion = await original.GetComputedStateVersionAsync(new DefaultProjectSnapshot(original)); + var originalTagHelpers = original.TagHelpers; + var originalProjectWorkspaceStateVersion = original.ProjectWorkspaceStateVersion; // Act - var state = original.WithWorkspaceProject(null); + var state = original.WithProjectWorkspaceState(null); // Assert Assert.NotEqual(original.Version, state.Version); - Assert.Null(state.WorkspaceProject); + Assert.Null(state.ProjectWorkspaceState); - var actualTagHelpers = await state.GetTagHelpersAsync(new DefaultProjectSnapshot(state)); - var actualComputedVersion = await state.GetComputedStateVersionAsync(new DefaultProjectSnapshot(state)); + var actualTagHelpers = state.TagHelpers; + var actualProjectWorkspaceStateVersion = state.ProjectWorkspaceStateVersion; // The configuration didn't change, and the tag helpers didn't actually change Assert.Same(original.ProjectEngine, state.ProjectEngine); Assert.Same(originalTagHelpers, actualTagHelpers); - Assert.Equal(originalComputedVersion, actualComputedVersion); + Assert.NotEqual(originalProjectWorkspaceStateVersion, actualProjectWorkspaceStateVersion); Assert.NotSame(original.Documents[Documents[1].FilePath], state.Documents[Documents[1].FilePath]); Assert.NotSame(original.Documents[Documents[2].FilePath], state.Documents[Documents[2].FilePath]); } [Fact] - public async Task ProjectState_WithWorkspaceProject_Added() + public void ProjectState_WithProjectWorkspaceState_Added() { // Arrange var original = ProjectState.Create(Workspace.Services, HostProject, null) @@ -629,115 +622,116 @@ public async Task ProjectState_WithWorkspaceProject_Added() .WithAddedHostDocument(Documents[1], DocumentState.EmptyLoader); // Force init - var originalTagHelpers = await original.GetTagHelpersAsync(new DefaultProjectSnapshot(original)); - var originalComputedVersion = await original.GetComputedStateVersionAsync(new DefaultProjectSnapshot(original)); + var originalTagHelpers = original.TagHelpers; + var originalProjectWorkspaceStateVersion = original.ProjectWorkspaceStateVersion; + var newProjectWorkspaceState = ProjectWorkspaceState.Default; // Act - var state = original.WithWorkspaceProject(WorkspaceProject); + var state = original.WithProjectWorkspaceState(newProjectWorkspaceState); // Assert Assert.NotEqual(original.Version, state.Version); - Assert.Same(WorkspaceProject, state.WorkspaceProject); + Assert.Same(newProjectWorkspaceState, state.ProjectWorkspaceState); - var actualTagHelpers = await state.GetTagHelpersAsync(new DefaultProjectSnapshot(state)); - var actualComputedVersion = await state.GetComputedStateVersionAsync(new DefaultProjectSnapshot(state)); + var actualTagHelpers = state.TagHelpers; + var actualProjectWorkspaceStateVersion = state.ProjectWorkspaceStateVersion; // The configuration didn't change, and the tag helpers didn't actually change Assert.Same(original.ProjectEngine, state.ProjectEngine); Assert.Same(originalTagHelpers, actualTagHelpers); - Assert.Equal(originalComputedVersion, actualComputedVersion); + Assert.NotEqual(originalProjectWorkspaceStateVersion, actualProjectWorkspaceStateVersion); 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 async Task ProjectState_WithWorkspaceProject_Changed() + public void ProjectState_WithProjectWorkspaceState_Changed() { // Arrange - var original = ProjectState.Create(Workspace.Services, HostProject, WorkspaceProject) + var original = ProjectState.Create(Workspace.Services, HostProject, ProjectWorkspaceState) .WithAddedHostDocument(Documents[2], DocumentState.EmptyLoader) .WithAddedHostDocument(Documents[1], DocumentState.EmptyLoader); // Force init - var originalTagHelpers = await original.GetTagHelpersAsync(new DefaultProjectSnapshot(original)); - var originalComputedVersion = await original.GetComputedStateVersionAsync(new DefaultProjectSnapshot(original)); + var originalTagHelpers = original.TagHelpers; + var originalProjectWorkspaceStateVersion = original.ProjectWorkspaceStateVersion; - var changed = WorkspaceProject.WithAssemblyName("Test1"); + var changed = new ProjectWorkspaceState(ProjectWorkspaceState.TagHelpers); // Act - var state = original.WithWorkspaceProject(changed); + var state = original.WithProjectWorkspaceState(changed); // Assert Assert.NotEqual(original.Version, state.Version); - Assert.Same(changed, state.WorkspaceProject); + Assert.Same(changed, state.ProjectWorkspaceState); - var actualTagHelpers = await state.GetTagHelpersAsync(new DefaultProjectSnapshot(state)); - var actualComputedVersion = await state.GetComputedStateVersionAsync(new DefaultProjectSnapshot(state)); + var actualTagHelpers = state.TagHelpers; + var actualProjectWorkspaceStateVersion = state.ProjectWorkspaceStateVersion; // The configuration didn't change, and the tag helpers didn't actually change Assert.Same(original.ProjectEngine, state.ProjectEngine); Assert.Same(originalTagHelpers, actualTagHelpers); - Assert.Equal(originalComputedVersion, actualComputedVersion); + Assert.Equal(originalProjectWorkspaceStateVersion, actualProjectWorkspaceStateVersion); Assert.NotSame(original.Documents[Documents[1].FilePath], state.Documents[Documents[1].FilePath]); Assert.NotSame(original.Documents[Documents[2].FilePath], state.Documents[Documents[2].FilePath]); } [Fact] - public async Task ProjectState_WithWorkspaceProject_Changed_TagHelpersChanged() + public void ProjectState_WithProjectWorkspaceState_Changed_TagHelpersChanged() { // Arrange - var original = ProjectState.Create(Workspace.Services, HostProject, WorkspaceProject) + var original = ProjectState.Create(Workspace.Services, HostProject, ProjectWorkspaceState) .WithAddedHostDocument(Documents[2], DocumentState.EmptyLoader) .WithAddedHostDocument(Documents[1], DocumentState.EmptyLoader); // Force init - var originalTagHelpers = await original.GetTagHelpersAsync(new DefaultProjectSnapshot(original)); - var originalComputedVersion = await original.GetComputedStateVersionAsync(new DefaultProjectSnapshot(original)); + var originalTagHelpers = original.TagHelpers; + var originalProjectWorkspaceStateVersion = original.ProjectWorkspaceStateVersion; - var changed = WorkspaceProject.WithAssemblyName("Test1"); + var changed = new ProjectWorkspaceState(Array.Empty()); // Now create some tag helpers TagHelperResolver.TagHelpers = SomeTagHelpers; // Act - var state = original.WithWorkspaceProject(changed); + var state = original.WithProjectWorkspaceState(changed); // Assert Assert.NotEqual(original.Version, state.Version); - Assert.Same(changed, state.WorkspaceProject); + Assert.Same(changed, state.ProjectWorkspaceState); - var actualTagHelpers = await state.GetTagHelpersAsync(new DefaultProjectSnapshot(state)); - var actualComputedVersion = await state.GetComputedStateVersionAsync(new DefaultProjectSnapshot(state)); + var actualTagHelpers = state.TagHelpers; + var actualProjectWorkspaceStateVersion = state.ProjectWorkspaceStateVersion; // The configuration didn't change, but the tag helpers did Assert.Same(original.ProjectEngine, state.ProjectEngine); Assert.NotEqual(originalTagHelpers, actualTagHelpers); - Assert.NotEqual(originalComputedVersion, actualComputedVersion); - Assert.Equal(state.Version, actualComputedVersion); + Assert.NotEqual(originalProjectWorkspaceStateVersion, actualProjectWorkspaceStateVersion); + Assert.Equal(state.Version, actualProjectWorkspaceStateVersion); Assert.NotSame(original.Documents[Documents[1].FilePath], state.Documents[Documents[1].FilePath]); Assert.NotSame(original.Documents[Documents[2].FilePath], state.Documents[Documents[2].FilePath]); } [Fact] - public void ProjectState_WithWorkspaceProject_CallsWorkspaceProjectChangeOnDocumentState() + public void ProjectState_WithProjectWorkspaceState_CallsWorkspaceProjectChangeOnDocumentState() { // Arrange var callCount = 0; var documents = ImmutableDictionary.CreateBuilder(FilePathComparer.Instance); - documents[Documents[1].FilePath] = TestDocumentState.Create(Workspace.Services, Documents[1], onWorkspaceProjectChange: () => callCount++); - documents[Documents[2].FilePath] = TestDocumentState.Create(Workspace.Services, Documents[2], onWorkspaceProjectChange: () => callCount++); + documents[Documents[1].FilePath] = TestDocumentState.Create(Workspace.Services, Documents[1], onProjectWorkspaceStateChange: () => callCount++); + documents[Documents[2].FilePath] = TestDocumentState.Create(Workspace.Services, Documents[2], onProjectWorkspaceStateChange: () => callCount++); - var original = ProjectState.Create(Workspace.Services, HostProject, WorkspaceProject); + var original = ProjectState.Create(Workspace.Services, HostProject, ProjectWorkspaceState); original.Documents = documents.ToImmutable(); - var changed = WorkspaceProject.WithAssemblyName("Test1"); + var changed = new ProjectWorkspaceState(Array.Empty()); // Act - var state = original.WithWorkspaceProject(changed); + var state = original.WithProjectWorkspaceState(changed); // Assert Assert.NotEqual(original.Version, state.Version); @@ -749,7 +743,7 @@ public void ProjectState_WhenImportDocumentAdded_CallsImportsChanged() { // Arrange var callCount = 0; - + var document1 = TestProjectData.SomeProjectFile1; var document2 = TestProjectData.SomeProjectFile2; var document3 = TestProjectData.SomeProjectNestedFile3; @@ -763,7 +757,7 @@ public void ProjectState_WhenImportDocumentAdded_CallsImportsChanged() var importsToRelatedDocuments = ImmutableDictionary.CreateBuilder>(FilePathComparer.Instance); importsToRelatedDocuments.Add( - TestProjectData.SomeProjectImportFile.TargetPath, + TestProjectData.SomeProjectImportFile.TargetPath, ImmutableArray.Create( TestProjectData.SomeProjectFile1.FilePath, TestProjectData.SomeProjectFile2.FilePath, @@ -775,7 +769,7 @@ public void ProjectState_WhenImportDocumentAdded_CallsImportsChanged() TestProjectData.SomeProjectNestedFile3.FilePath, TestProjectData.AnotherProjectNestedFile4.FilePath)); - var original = ProjectState.Create(Workspace.Services, HostProject, WorkspaceProject); + var original = ProjectState.Create(Workspace.Services, HostProject, ProjectWorkspaceState); original.Documents = documents.ToImmutable(); original.ImportsToRelatedDocuments = importsToRelatedDocuments.ToImmutable(); @@ -818,7 +812,7 @@ public void ProjectState_WhenImportDocumentAdded_CallsImportsChanged_Nested() TestProjectData.SomeProjectNestedFile3.FilePath, TestProjectData.AnotherProjectNestedFile4.FilePath)); - var original = ProjectState.Create(Workspace.Services, HostProject, WorkspaceProject); + var original = ProjectState.Create(Workspace.Services, HostProject, ProjectWorkspaceState); original.Documents = documents.ToImmutable(); original.ImportsToRelatedDocuments = importsToRelatedDocuments.ToImmutable(); @@ -864,7 +858,7 @@ public void ProjectState_WhenImportDocumentChangedTextLoader_CallsImportsChanged TestProjectData.SomeProjectNestedFile3.FilePath, TestProjectData.AnotherProjectNestedFile4.FilePath)); - var original = ProjectState.Create(Workspace.Services, HostProject, WorkspaceProject); + var original = ProjectState.Create(Workspace.Services, HostProject, ProjectWorkspaceState); original.Documents = documents.ToImmutable(); original.ImportsToRelatedDocuments = importsToRelatedDocuments.ToImmutable(); @@ -910,7 +904,7 @@ public void ProjectState_WhenImportDocumentChangedSnapshot_CallsImportsChanged() TestProjectData.SomeProjectNestedFile3.FilePath, TestProjectData.AnotherProjectNestedFile4.FilePath)); - var original = ProjectState.Create(Workspace.Services, HostProject, WorkspaceProject); + var original = ProjectState.Create(Workspace.Services, HostProject, ProjectWorkspaceState); original.Documents = documents.ToImmutable(); original.ImportsToRelatedDocuments = importsToRelatedDocuments.ToImmutable(); @@ -957,7 +951,7 @@ public void ProjectState_WhenImportDocumentRemoved_CallsImportsChanged() TestProjectData.SomeProjectNestedFile3.FilePath, TestProjectData.AnotherProjectNestedFile4.FilePath)); - var original = ProjectState.Create(Workspace.Services, HostProject, WorkspaceProject); + var original = ProjectState.Create(Workspace.Services, HostProject, ProjectWorkspaceState); original.Documents = documents.ToImmutable(); original.ImportsToRelatedDocuments = importsToRelatedDocuments.ToImmutable(); @@ -979,26 +973,26 @@ public static TestDocumentState Create( Action onTextLoaderChange = null, Action onConfigurationChange = null, Action onImportsChange = null, - Action onWorkspaceProjectChange = null) + Action onProjectWorkspaceStateChange = null) { return new TestDocumentState( - services, - hostDocument, - null, - null, - loader, - onTextChange, - onTextLoaderChange, - onConfigurationChange, + services, + hostDocument, + null, + null, + loader, + onTextChange, + onTextLoaderChange, + onConfigurationChange, onImportsChange, - onWorkspaceProjectChange); + onProjectWorkspaceStateChange); } private readonly Action _onTextChange; private readonly Action _onTextLoaderChange; private readonly Action _onConfigurationChange; private readonly Action _onImportsChange; - private readonly Action _onWorkspaceProjectChange; + private readonly Action _onProjectWorkspaceStateChange; private TestDocumentState( HostWorkspaceServices services, @@ -1010,14 +1004,14 @@ private TestDocumentState( Action onTextLoaderChange, Action onConfigurationChange, Action onImportsChange, - Action onWorkspaceProjectChange) + Action onProjectWorkspaceStateChange) : base(services, hostDocument, text, version, loader) { _onTextChange = onTextChange; _onTextLoaderChange = onTextLoaderChange; _onConfigurationChange = onConfigurationChange; _onImportsChange = onImportsChange; - _onWorkspaceProjectChange = onWorkspaceProjectChange; + _onProjectWorkspaceStateChange = onProjectWorkspaceStateChange; } public override DocumentState WithText(SourceText sourceText, VersionStamp version) @@ -1044,10 +1038,10 @@ public override DocumentState WithImportsChange() return base.WithImportsChange(); } - public override DocumentState WithWorkspaceProjectChange() + public override DocumentState WithProjectWorkspaceStateChange() { - _onWorkspaceProjectChange?.Invoke(); - return base.WithWorkspaceProjectChange(); + _onProjectWorkspaceStateChange?.Invoke(); + return base.WithProjectWorkspaceStateChange(); } } } diff --git a/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Shared/TestTagHelperResolver.cs b/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Shared/TestTagHelperResolver.cs index 82fccb653bc..e96417cf786 100644 --- a/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Shared/TestTagHelperResolver.cs +++ b/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Shared/TestTagHelperResolver.cs @@ -15,9 +15,9 @@ internal class TestTagHelperResolver : TagHelperResolver { public TaskCompletionSource CompletionSource { get; set; } - public IList TagHelpers { get; set; } = new List(); + public List TagHelpers { get; set; } = new List(); - public override Task GetTagHelpersAsync(ProjectSnapshot project, CancellationToken cancellationToken = default) + public override Task GetTagHelpersAsync(Project workspaceProject, ProjectSnapshot projectSnapshot, CancellationToken cancellationToken = default) { if (CompletionSource == null) { diff --git a/src/Razor/test/Microsoft.VisualStudio.Editor.Razor.Test.Common/TestProjectWorkspaceStateGenerator.cs b/src/Razor/test/Microsoft.VisualStudio.Editor.Razor.Test.Common/TestProjectWorkspaceStateGenerator.cs new file mode 100644 index 00000000000..5d735853085 --- /dev/null +++ b/src/Razor/test/Microsoft.VisualStudio.Editor.Razor.Test.Common/TestProjectWorkspaceStateGenerator.cs @@ -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.Collections.Generic; +using Microsoft.CodeAnalysis.Razor; +using Microsoft.CodeAnalysis.Razor.ProjectSystem; +using Microsoft.CodeAnalysis; + +namespace Microsoft.VisualStudio.Editor.Razor.Test +{ + internal class TestProjectWorkspaceStateGenerator : ProjectWorkspaceStateGenerator + { + private List<(Project workspaceProject, ProjectSnapshot projectSnapshot)> _updates; + + public TestProjectWorkspaceStateGenerator() + { + _updates = new List<(Project workspaceProject, ProjectSnapshot projectSnapshot)>(); + } + + public IReadOnlyList<(Project workspaceProject, ProjectSnapshot projectSnapshot)> UpdateQueue => _updates; + + public override void Initialize(ProjectSnapshotManagerBase projectManager) + { + } + + public override void Update(Project workspaceProject, ProjectSnapshot projectSnapshot) + { + var update = (workspaceProject, projectSnapshot); + _updates.Add(update); + } + } +} diff --git a/src/Razor/test/Microsoft.VisualStudio.Editor.Razor.Test/DefaultProjectSnapshotProjectEngineFactoryTest.cs b/src/Razor/test/Microsoft.VisualStudio.Editor.Razor.Test/DefaultProjectSnapshotProjectEngineFactoryTest.cs index c4eb90c2a46..c98af8ca5ff 100644 --- a/src/Razor/test/Microsoft.VisualStudio.Editor.Razor.Test/DefaultProjectSnapshotProjectEngineFactoryTest.cs +++ b/src/Razor/test/Microsoft.VisualStudio.Editor.Razor.Test/DefaultProjectSnapshotProjectEngineFactoryTest.cs @@ -20,15 +20,9 @@ public class DefaultProjectSnapshotProjectEngineFactoryTest { public DefaultProjectSnapshotProjectEngineFactoryTest() { - Project project = null; + Workspace = TestWorkspace.Create(); - Workspace = TestWorkspace.Create(workspace => - { - var info = ProjectInfo.Create(ProjectId.CreateNewId("Test"), VersionStamp.Default, "Test", "Test", LanguageNames.CSharp, filePath: "/TestPath/SomePath/Test.csproj"); - project = workspace.CurrentSolution.AddProject(info).GetProject(info.Id); - }); - - WorkspaceProject = project; + ProjectWorkspaceState = ProjectWorkspaceState.Default; HostProject_For_1_0 = new HostProject("/TestPath/SomePath/Test.csproj", FallbackRazorConfiguration.MVC_1_0); HostProject_For_1_1 = new HostProject("/TestPath/SomePath/Test.csproj", FallbackRazorConfiguration.MVC_1_1); @@ -46,12 +40,12 @@ public DefaultProjectSnapshotProjectEngineFactoryTest() "/TestPath/SomePath/Test.csproj", new ProjectSystemRazorConfiguration(RazorLanguageVersion.Version_2_1, "Blazor-0.1", Array.Empty())); - Snapshot_For_1_0 = new DefaultProjectSnapshot(ProjectState.Create(Workspace.Services, HostProject_For_1_0, WorkspaceProject)); - Snapshot_For_1_1 = new DefaultProjectSnapshot(ProjectState.Create(Workspace.Services, HostProject_For_1_1, WorkspaceProject)); - Snapshot_For_2_0 = new DefaultProjectSnapshot(ProjectState.Create(Workspace.Services, HostProject_For_2_0, WorkspaceProject)); - Snapshot_For_2_1 = new DefaultProjectSnapshot(ProjectState.Create(Workspace.Services, HostProject_For_2_1, WorkspaceProject)); - Snapshot_For_3_0 = new DefaultProjectSnapshot(ProjectState.Create(Workspace.Services, HostProject_For_3_0, WorkspaceProject)); - Snapshot_For_UnknownConfiguration = new DefaultProjectSnapshot(ProjectState.Create(Workspace.Services, HostProject_For_UnknownConfiguration, WorkspaceProject)); + Snapshot_For_1_0 = new DefaultProjectSnapshot(ProjectState.Create(Workspace.Services, HostProject_For_1_0, ProjectWorkspaceState)); + Snapshot_For_1_1 = new DefaultProjectSnapshot(ProjectState.Create(Workspace.Services, HostProject_For_1_1, ProjectWorkspaceState)); + Snapshot_For_2_0 = new DefaultProjectSnapshot(ProjectState.Create(Workspace.Services, HostProject_For_2_0, ProjectWorkspaceState)); + Snapshot_For_2_1 = new DefaultProjectSnapshot(ProjectState.Create(Workspace.Services, HostProject_For_2_1, ProjectWorkspaceState)); + Snapshot_For_3_0 = new DefaultProjectSnapshot(ProjectState.Create(Workspace.Services, HostProject_For_3_0, ProjectWorkspaceState)); + Snapshot_For_UnknownConfiguration = new DefaultProjectSnapshot(ProjectState.Create(Workspace.Services, HostProject_For_UnknownConfiguration, ProjectWorkspaceState)); CustomFactories = new Lazy[] { @@ -103,7 +97,7 @@ public DefaultProjectSnapshotProjectEngineFactoryTest() private ProjectSnapshot Snapshot_For_UnknownConfiguration { get; } - private Project WorkspaceProject { get; } + private ProjectWorkspaceState ProjectWorkspaceState { get; } private Workspace Workspace { get; } diff --git a/src/Razor/test/Microsoft.VisualStudio.Editor.Razor.Test/DefaultProjectWorkspaceStateGeneratorTest.cs b/src/Razor/test/Microsoft.VisualStudio.Editor.Razor.Test/DefaultProjectWorkspaceStateGeneratorTest.cs new file mode 100644 index 00000000000..9ce94e106bc --- /dev/null +++ b/src/Razor/test/Microsoft.VisualStudio.Editor.Razor.Test/DefaultProjectWorkspaceStateGeneratorTest.cs @@ -0,0 +1,134 @@ +// 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.Host; +using Microsoft.CodeAnalysis.Razor.ProjectSystem; +using Xunit; + +namespace Microsoft.CodeAnalysis.Razor.Workspaces +{ + public class DefaultProjectWorkspaceStateGeneratorTest : ForegroundDispatcherTestBase + { + public DefaultProjectWorkspaceStateGeneratorTest() + { + var tagHelperResolver = new TestTagHelperResolver(); + tagHelperResolver.TagHelpers.Add(TagHelperDescriptorBuilder.Create("ResolvableTagHelper", "TestAssembly").Build()); + ResolvableTagHelpers = tagHelperResolver.TagHelpers; + var languageServices = new List() { tagHelperResolver }; + var testServices = TestServices.Create(languageServices); + Workspace = TestWorkspace.Create(testServices); + var projectId = ProjectId.CreateNewId("Test"); + var solution = Workspace.CurrentSolution.AddProject(ProjectInfo.Create( + projectId, + VersionStamp.Default, + "Test", + "Test", + LanguageNames.CSharp, + TestProjectData.SomeProject.FilePath)); + WorkspaceProject = solution.GetProject(projectId); + ProjectSnapshot = new DefaultProjectSnapshot(ProjectState.Create(Workspace.Services, TestProjectData.SomeProject)); + ProjectWorkspaceStateWithTagHelpers = new ProjectWorkspaceState(new[] + { + TagHelperDescriptorBuilder.Create("TestTagHelper", "TestAssembly").Build(), + }); + } + + private IReadOnlyList ResolvableTagHelpers { get; } + + private Workspace Workspace { get; } + + private Project WorkspaceProject { get; } + + private DefaultProjectSnapshot ProjectSnapshot { get; } + + private ProjectWorkspaceState ProjectWorkspaceStateWithTagHelpers { get; } + + [ForegroundFact] + public void Update_StartsUpdateTask() + { + // Arrange + using (var stateGenerator = new DefaultProjectWorkspaceStateGenerator(Dispatcher)) + { + stateGenerator.BlockBackgroundWorkStart = new ManualResetEventSlim(initialState: false); + + // Act + stateGenerator.Update(WorkspaceProject, ProjectSnapshot); + + // Assert + var update = Assert.Single(stateGenerator._updates); + Assert.False(update.Value.Task.IsCompleted); + } + } + + [ForegroundFact] + public void Update_SoftCancelsIncompleteTaskForSameProject() + { + // Arrange + using (var stateGenerator = new DefaultProjectWorkspaceStateGenerator(Dispatcher)) + { + stateGenerator.BlockBackgroundWorkStart = new ManualResetEventSlim(initialState: false); + stateGenerator.Update(WorkspaceProject, ProjectSnapshot); + var initialUpdate = stateGenerator._updates.Single().Value; + + // Act + stateGenerator.Update(WorkspaceProject, ProjectSnapshot); + + // Assert + Assert.True(initialUpdate.Cts.IsCancellationRequested); + } + } + + [ForegroundFact] + public async Task Update_NullWorkspaceProject_ClearsProjectWorkspaceState() + { + // Arrange + using (var stateGenerator = new DefaultProjectWorkspaceStateGenerator(Dispatcher)) + { + stateGenerator.NotifyBackgroundWorkCompleted = new ManualResetEventSlim(initialState: false); + var projectManager = new TestProjectSnapshotManager(Dispatcher, Workspace); + stateGenerator.Initialize(projectManager); + projectManager.ProjectAdded(ProjectSnapshot.HostProject); + projectManager.ProjectWorkspaceStateChanged(ProjectSnapshot.FilePath, ProjectWorkspaceStateWithTagHelpers); + + // Act + stateGenerator.Update(workspaceProject: null, ProjectSnapshot); + + // Jump off the foreground thread so the background work can complete. + await Task.Run(() => stateGenerator.NotifyBackgroundWorkCompleted.Wait(TimeSpan.FromSeconds(3))); + + // Assert + var newProjectSnapshot = projectManager.GetLoadedProject(ProjectSnapshot.FilePath); + Assert.Empty(newProjectSnapshot.TagHelpers); + } + } + + [ForegroundFact] + public async Task Update_ResolvesTagHelpersAndUpdatesWorkspaceState() + { + // Arrange + using (var stateGenerator = new DefaultProjectWorkspaceStateGenerator(Dispatcher)) + { + stateGenerator.NotifyBackgroundWorkCompleted = new ManualResetEventSlim(initialState: false); + var projectManager = new TestProjectSnapshotManager(Dispatcher, Workspace); + stateGenerator.Initialize(projectManager); + projectManager.ProjectAdded(ProjectSnapshot.HostProject); + + // Act + stateGenerator.Update(WorkspaceProject, ProjectSnapshot); + + // Jump off the foreground thread so the background work can complete. + await Task.Run(() => stateGenerator.NotifyBackgroundWorkCompleted.Wait(TimeSpan.FromSeconds(3))); + + // Assert + var newProjectSnapshot = projectManager.GetLoadedProject(ProjectSnapshot.FilePath); + Assert.Equal(ResolvableTagHelpers, newProjectSnapshot.TagHelpers); + } + } + } +} diff --git a/src/Razor/test/Microsoft.VisualStudio.Editor.Razor.Test/DefaultVisualStudioDocumentTrackerTest.cs b/src/Razor/test/Microsoft.VisualStudio.Editor.Razor.Test/DefaultVisualStudioDocumentTrackerTest.cs index b5d63b1b34d..993937d0ac7 100644 --- a/src/Razor/test/Microsoft.VisualStudio.Editor.Razor.Test/DefaultVisualStudioDocumentTrackerTest.cs +++ b/src/Razor/test/Microsoft.VisualStudio.Editor.Razor.Test/DefaultVisualStudioDocumentTrackerTest.cs @@ -110,15 +110,11 @@ public void Subscribe_NoopsIfAlreadySubscribed() }; DocumentTracker.Subscribe(); - // Call count is 2 right now: - // 1 trigger for initial subscribe context changed. - // 1 trigger for TagHelpers being changed (computed). - // Act DocumentTracker.Subscribe(); // Assert - Assert.Equal(2, callCount); + Assert.Equal(1, callCount); } [ForegroundFact] @@ -188,15 +184,13 @@ public void EditorSettingsManager_Changed_TriggersContextChanged() public void ProjectManager_Changed_ProjectAdded_TriggersContextChanged() { // Arrange - ProjectManager.HostProjectAdded(HostProject); - ProjectManager.WorkspaceProjectAdded(WorkspaceProject); + ProjectManager.ProjectAdded(HostProject); var e = new ProjectChangeEventArgs(null, ProjectManager.GetLoadedProject(HostProject.FilePath), ProjectChangeKind.ProjectAdded); var called = false; DocumentTracker.ContextChanged += (sender, args) => { - Assert.Equal(ContextChangeKind.ProjectChanged, args.Kind); called = true; Assert.Same(ProjectManager.GetLoadedProject(DocumentTracker.ProjectPath), DocumentTracker.ProjectSnapshot); @@ -213,15 +207,13 @@ public void ProjectManager_Changed_ProjectAdded_TriggersContextChanged() public void ProjectManager_Changed_ProjectChanged_TriggersContextChanged() { // Arrange - ProjectManager.HostProjectAdded(HostProject); - ProjectManager.WorkspaceProjectAdded(WorkspaceProject); + ProjectManager.ProjectAdded(HostProject); var e = new ProjectChangeEventArgs(null, ProjectManager.GetLoadedProject(HostProject.FilePath), ProjectChangeKind.ProjectChanged); var called = false; DocumentTracker.ContextChanged += (sender, args) => { - Assert.Equal(ContextChangeKind.ProjectChanged, args.Kind); called = true; Assert.Same(ProjectManager.GetLoadedProject(DocumentTracker.ProjectPath), DocumentTracker.ProjectSnapshot); @@ -238,11 +230,10 @@ public void ProjectManager_Changed_ProjectChanged_TriggersContextChanged() public void ProjectManager_Changed_ProjectRemoved_TriggersContextChanged_WithEphemeralProject() { // Arrange - ProjectManager.HostProjectAdded(HostProject); - ProjectManager.WorkspaceProjectAdded(WorkspaceProject); + ProjectManager.ProjectAdded(HostProject); var project = ProjectManager.GetLoadedProject(HostProject.FilePath); - ProjectManager.HostProjectRemoved(HostProject); + ProjectManager.ProjectRemoved(HostProject); var e = new ProjectChangeEventArgs(project, null, ProjectChangeKind.ProjectRemoved); @@ -266,7 +257,7 @@ public void ProjectManager_Changed_ProjectRemoved_TriggersContextChanged_WithEph public void ProjectManager_Changed_IgnoresUnknownProject() { // Arrange - ProjectManager.HostProjectAdded(OtherHostProject); + ProjectManager.ProjectAdded(OtherHostProject); var e = new ProjectChangeEventArgs(null, ProjectManager.GetLoadedProject(OtherHostProject.FilePath), ProjectChangeKind.ProjectChanged); @@ -471,7 +462,7 @@ public void Subscribed_InitializesEphemeralProjectSnapshot() public void Subscribed_InitializesRealProjectSnapshot() { // Arrange - ProjectManager.HostProjectAdded(HostProject); + ProjectManager.ProjectAdded(HostProject); // Act DocumentTracker.Subscribe(); @@ -481,23 +472,18 @@ public void Subscribed_InitializesRealProjectSnapshot() } [ForegroundFact] - public async Task Subscribed_ListensToProjectChanges() + public void Subscribed_ListensToProjectChanges() { // Arrange - ProjectManager.HostProjectAdded(HostProject); + ProjectManager.ProjectAdded(HostProject); DocumentTracker.Subscribe(); - await DocumentTracker.PendingTagHelperTask; - - // There can be multiple args here because the tag helpers will return - // immediately and trigger another ContextChanged. - List args = new List(); + var args = new List(); DocumentTracker.ContextChanged += (sender, e) => { args.Add(e); }; // Act - ProjectManager.HostProjectChanged(UpdatedHostProject); - await DocumentTracker.PendingTagHelperTask; + ProjectManager.ProjectConfigurationChanged(UpdatedHostProject); // Assert var snapshot = Assert.IsType(DocumentTracker.ProjectSnapshot); @@ -506,63 +492,29 @@ public async Task Subscribed_ListensToProjectChanges() Assert.Collection( args, - e => Assert.Equal(ContextChangeKind.ProjectChanged, e.Kind), - e => Assert.Equal(ContextChangeKind.TagHelpersChanged, e.Kind)); + e => Assert.Equal(ContextChangeKind.ProjectChanged, e.Kind)); } [ForegroundFact] - public async Task Subscribed_ListensToProjectRemoval() + public void Subscribed_ListensToProjectRemoval() { // Arrange - ProjectManager.HostProjectAdded(HostProject); + ProjectManager.ProjectAdded(HostProject); DocumentTracker.Subscribe(); - await DocumentTracker.PendingTagHelperTask; - - List args = new List(); + var args = new List(); DocumentTracker.ContextChanged += (sender, e) => { args.Add(e); }; // Act - ProjectManager.HostProjectRemoved(HostProject); - await DocumentTracker.PendingTagHelperTask; + ProjectManager.ProjectRemoved(HostProject); // Assert Assert.IsType(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(); - - ProjectManager.HostProjectAdded(HostProject); - - DocumentTracker.Subscribe(); - - // We haven't let the tag helpers complete yet - Assert.False(DocumentTracker.PendingTagHelperTask.IsCompleted); - Assert.Empty(DocumentTracker.TagHelpers); - - List args = new List(); - DocumentTracker.ContextChanged += (sender, e) => { args.Add(e); }; - - // Act - TagHelperResolver.CompletionSource.SetResult(new TagHelperResolutionResult(SomeTagHelpers, Array.Empty())); - await DocumentTracker.PendingTagHelperTask; - - // Assert - Assert.Same(SomeTagHelpers, DocumentTracker.TagHelpers); - - Assert.Collection( - args, - e => Assert.Equal(ContextChangeKind.TagHelpersChanged, e.Kind)); + e => Assert.Equal(ContextChangeKind.ProjectChanged, e.Kind)); } } } diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/DocumentGenerator/BackgroundDocumentGeneratorTest.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/DocumentGenerator/BackgroundDocumentGeneratorTest.cs index 8b67f86ec62..5901dfb92aa 100644 --- a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/DocumentGenerator/BackgroundDocumentGeneratorTest.cs +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/DocumentGenerator/BackgroundDocumentGeneratorTest.cs @@ -27,28 +27,6 @@ public BackgroundDocumentGeneratorTest() HostProject1 = new HostProject(TestProjectData.SomeProject.FilePath, FallbackRazorConfiguration.MVC_1_0); HostProject2 = new HostProject(TestProjectData.AnotherProject.FilePath, FallbackRazorConfiguration.MVC_1_0); - var projectId1 = ProjectId.CreateNewId("Test1"); - var projectId2 = ProjectId.CreateNewId("Test2"); - - var solution = Workspace.CurrentSolution - .AddProject(ProjectInfo.Create( - projectId1, - VersionStamp.Default, - "Test1", - "Test1", - LanguageNames.CSharp, - TestProjectData.SomeProject.FilePath)) - .AddProject(ProjectInfo.Create( - projectId2, - VersionStamp.Default, - "Test2", - "Test2", - LanguageNames.CSharp, - TestProjectData.AnotherProject.FilePath)); ; - - WorkspaceProject1 = solution.GetProject(projectId1); - WorkspaceProject2 = solution.GetProject(projectId2); - DynamicFileInfoProvider = new RazorDynamicFileInfoProvider(new DefaultDocumentServiceProviderFactory()); } @@ -58,10 +36,6 @@ public BackgroundDocumentGeneratorTest() private HostProject HostProject2 { get; } - private Project WorkspaceProject1 { get; } - - private Project WorkspaceProject2 { get; } - private RazorDynamicFileInfoProvider DynamicFileInfoProvider { get; } protected override void ConfigureProjectEngine(RazorProjectEngineBuilder builder) @@ -74,10 +48,8 @@ public async Task Queue_ProcessesNotifications_AndGoesBackToSleep() { // Arrange var projectManager = new TestProjectSnapshotManager(Dispatcher, Workspace); - projectManager.HostProjectAdded(HostProject1); - projectManager.HostProjectAdded(HostProject2); - projectManager.WorkspaceProjectAdded(WorkspaceProject1); - projectManager.WorkspaceProjectAdded(WorkspaceProject2); + projectManager.ProjectAdded(HostProject1); + projectManager.ProjectAdded(HostProject2); projectManager.DocumentAdded(HostProject1, Documents[0], null); projectManager.DocumentAdded(HostProject1, Documents[1], null); @@ -115,10 +87,8 @@ public async Task Queue_ProcessesNotifications_AndRestarts() { // Arrange var projectManager = new TestProjectSnapshotManager(Dispatcher, Workspace); - projectManager.HostProjectAdded(HostProject1); - projectManager.HostProjectAdded(HostProject2); - projectManager.WorkspaceProjectAdded(WorkspaceProject1); - projectManager.WorkspaceProjectAdded(WorkspaceProject2); + projectManager.ProjectAdded(HostProject1); + projectManager.ProjectAdded(HostProject2); projectManager.DocumentAdded(HostProject1, Documents[0], null); projectManager.DocumentAdded(HostProject1, Documents[1], null); diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/OOPTagHelperResolverTest.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/OOPTagHelperResolverTest.cs index bf81c4e5a84..f252fd44bd7 100644 --- a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/OOPTagHelperResolverTest.cs +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/OOPTagHelperResolverTest.cs @@ -66,43 +66,26 @@ public OOPTagHelperResolverTest() private Workspace Workspace { get; } - [Fact] - public async Task GetTagHelpersAsync_WithNonInitializedProject_Noops() - { - // Arrange - ProjectManager.HostProjectAdded(HostProject_For_2_0); - - var project = ProjectManager.GetLoadedProject("Test.csproj"); - - var resolver = new TestTagHelperResolver(EngineFactory, ErrorReporter, Workspace); - - var result = await resolver.GetTagHelpersAsync(project); - - // Assert - Assert.Same(TagHelperResolutionResult.Empty, result); - } - [Fact] public async Task GetTagHelpersAsync_WithSerializableCustomFactory_GoesOutOfProcess() { // Arrange - ProjectManager.HostProjectAdded(HostProject_For_2_0); - ProjectManager.WorkspaceProjectAdded(WorkspaceProject); + ProjectManager.ProjectAdded(HostProject_For_2_0); - var project = ProjectManager.GetLoadedProject("Test.csproj"); + var projectSnapshot = ProjectManager.GetLoadedProject("Test.csproj"); var resolver = new TestTagHelperResolver(EngineFactory, ErrorReporter, Workspace) { OnResolveOutOfProcess = (f, p) => { Assert.Same(CustomFactories[0].Value, f); - Assert.Same(project, p); + Assert.Same(projectSnapshot, p); return Task.FromResult(TagHelperResolutionResult.Empty); }, }; - var result = await resolver.GetTagHelpersAsync(project); + var result = await resolver.GetTagHelpersAsync(WorkspaceProject, projectSnapshot); // Assert Assert.Same(TagHelperResolutionResult.Empty, result); @@ -112,22 +95,21 @@ public async Task GetTagHelpersAsync_WithSerializableCustomFactory_GoesOutOfProc public async Task GetTagHelpersAsync_WithNonSerializableCustomFactory_StaysInProcess() { // Arrange - ProjectManager.HostProjectAdded(HostProject_For_NonSerializableConfiguration); - ProjectManager.WorkspaceProjectAdded(WorkspaceProject); + ProjectManager.ProjectAdded(HostProject_For_NonSerializableConfiguration); - var project = ProjectManager.GetLoadedProject("Test.csproj"); + var projectSnapshot = ProjectManager.GetLoadedProject("Test.csproj"); var resolver = new TestTagHelperResolver(EngineFactory, ErrorReporter, Workspace) { OnResolveInProcess = (p) => { - Assert.Same(project, p); + Assert.Same(projectSnapshot, p); return Task.FromResult(TagHelperResolutionResult.Empty); }, }; - var result = await resolver.GetTagHelpersAsync(project); + var result = await resolver.GetTagHelpersAsync(WorkspaceProject, projectSnapshot); // Assert Assert.Same(TagHelperResolutionResult.Empty, result); @@ -145,16 +127,16 @@ public TestTagHelperResolver(ProjectSnapshotProjectEngineFactory factory, ErrorR public Func> OnResolveInProcess { get; set; } - protected override Task ResolveTagHelpersOutOfProcessAsync(IProjectEngineFactory factory, ProjectSnapshot project) + protected override Task ResolveTagHelpersOutOfProcessAsync(IProjectEngineFactory factory, Project workspaceProject, ProjectSnapshot projectSnapshot) { Assert.NotNull(OnResolveOutOfProcess); - return OnResolveOutOfProcess(factory, project); + return OnResolveOutOfProcess(factory, projectSnapshot); } - protected override Task ResolveTagHelpersInProcessAsync(ProjectSnapshot project) + protected override Task ResolveTagHelpersInProcessAsync(Project project, ProjectSnapshot projectSnapshot) { Assert.NotNull(OnResolveInProcess); - return OnResolveInProcess(project); + return OnResolveInProcess(projectSnapshot); } } private class TestProjectSnapshotManager : DefaultProjectSnapshotManager diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/ProjectSystem/DefaultProjectSnapshotManagerTest.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/ProjectSystem/DefaultProjectSnapshotManagerTest.cs index adf641d8155..b983fe87efb 100644 --- a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/ProjectSystem/DefaultProjectSnapshotManagerTest.cs +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/ProjectSystem/DefaultProjectSnapshotManagerTest.cs @@ -17,10 +17,6 @@ public class DefaultProjectSnapshotManagerTest : ForegroundDispatcherWorkspaceTe { public DefaultProjectSnapshotManagerTest() { - // Force VB and C# to Load - GC.KeepAlive(typeof(Microsoft.CodeAnalysis.CSharp.SyntaxFactory)); - GC.KeepAlive(typeof(Microsoft.CodeAnalysis.VisualBasic.SyntaxFactory)); - TagHelperResolver = new TestTagHelperResolver(); Documents = new HostDocument[] @@ -37,51 +33,10 @@ public DefaultProjectSnapshotManagerTest() HostProject = new HostProject(TestProjectData.SomeProject.FilePath, FallbackRazorConfiguration.MVC_2_0); HostProjectWithConfigurationChange = new HostProject(TestProjectData.SomeProject.FilePath, FallbackRazorConfiguration.MVC_1_0); - + ProjectManager = new TestProjectSnapshotManager(Dispatcher, Enumerable.Empty(), Workspace); - var projectId = ProjectId.CreateNewId("Test"); - var solution = Workspace.CurrentSolution.AddProject(ProjectInfo.Create( - projectId, - VersionStamp.Default, - "Test", - "Test", - LanguageNames.CSharp, - TestProjectData.SomeProject.FilePath)); - WorkspaceProject = solution.GetProject(projectId); - - var vbProjectId = ProjectId.CreateNewId("VB"); - solution = solution.AddProject(ProjectInfo.Create( - vbProjectId, - VersionStamp.Default, - "VB", - "VB", - LanguageNames.VisualBasic, - "VB.vbproj")); - VBWorkspaceProject = solution.GetProject(vbProjectId); - - var projectWithoutFilePathId = ProjectId.CreateNewId("NoFile"); - solution = solution.AddProject(ProjectInfo.Create( - projectWithoutFilePathId, - VersionStamp.Default, - "NoFile", - "NoFile", - LanguageNames.CSharp)); - WorkspaceProjectWithoutFilePath = solution.GetProject(projectWithoutFilePathId); - - // Approximates a project with multi-targeting - var projectIdWithDifferentTfm = ProjectId.CreateNewId("TestWithDifferentTfm"); - solution = Workspace.CurrentSolution.AddProject(ProjectInfo.Create( - projectIdWithDifferentTfm, - VersionStamp.Default, - "Test (Different TFM)", - "Test", - LanguageNames.CSharp, - TestProjectData.SomeProject.FilePath)); - WorkspaceProjectWithDifferentTfm = solution.GetProject(projectIdWithDifferentTfm); - - SomeTagHelpers = TagHelperResolver.TagHelpers; - SomeTagHelpers.Add(TagHelperDescriptorBuilder.Create("Test1", "TestAssembly").Build()); + ProjectWorkspaceStateWithTagHelpers = new ProjectWorkspaceState(TagHelperResolver.TagHelpers); SourceText = SourceText.From("Hello world"); } @@ -92,13 +47,7 @@ public DefaultProjectSnapshotManagerTest() private HostProject HostProjectWithConfigurationChange { get; } - private Project WorkspaceProject { get; } - - private Project WorkspaceProjectWithDifferentTfm { get; } - - private Project WorkspaceProjectWithoutFilePath { get; } - - private Project VBWorkspaceProject { get; } + private ProjectWorkspaceState ProjectWorkspaceStateWithTagHelpers { get; } private TestTagHelperResolver TagHelperResolver { get; } @@ -106,8 +55,6 @@ public DefaultProjectSnapshotManagerTest() private SourceText SourceText { get; } - private IList SomeTagHelpers { get; } - protected override void ConfigureLanguageServices(List services) { services.Add(TagHelperResolver); @@ -117,8 +64,7 @@ protected override void ConfigureLanguageServices(List service public void DocumentAdded_AddsDocument() { // Arrange - ProjectManager.HostProjectAdded(HostProject); - ProjectManager.WorkspaceProjectAdded(WorkspaceProject); + ProjectManager.ProjectAdded(HostProject); ProjectManager.Reset(); // Act @@ -135,8 +81,7 @@ public void DocumentAdded_AddsDocument() public void DocumentAdded_AddsDocument_Legacy() { // Arrange - ProjectManager.HostProjectAdded(HostProject); - ProjectManager.WorkspaceProjectAdded(WorkspaceProject); + ProjectManager.ProjectAdded(HostProject); ProjectManager.Reset(); // Act @@ -145,7 +90,7 @@ public void DocumentAdded_AddsDocument_Legacy() // Assert var snapshot = ProjectManager.GetSnapshot(HostProject); Assert.Collection( - snapshot.DocumentFilePaths.OrderBy(f => f), + snapshot.DocumentFilePaths.OrderBy(f => f), d => { Assert.Equal(Documents[0].FilePath, d); @@ -159,8 +104,7 @@ public void DocumentAdded_AddsDocument_Legacy() public void DocumentAdded_AddsDocument_Component() { // Arrange - ProjectManager.HostProjectAdded(HostProject); - ProjectManager.WorkspaceProjectAdded(WorkspaceProject); + ProjectManager.ProjectAdded(HostProject); ProjectManager.Reset(); // Act @@ -183,8 +127,7 @@ public void DocumentAdded_AddsDocument_Component() public void DocumentAdded_IgnoresDuplicate() { // Arrange - ProjectManager.HostProjectAdded(HostProject); - ProjectManager.WorkspaceProjectAdded(WorkspaceProject); + ProjectManager.ProjectAdded(HostProject); ProjectManager.DocumentAdded(HostProject, Documents[0], null); ProjectManager.Reset(); @@ -215,8 +158,7 @@ public void DocumentAdded_IgnoresUnknownProject() public async Task DocumentAdded_NullLoader_HasEmptyText() { // Arrange - ProjectManager.HostProjectAdded(HostProject); - ProjectManager.WorkspaceProjectAdded(WorkspaceProject); + ProjectManager.ProjectAdded(HostProject); ProjectManager.Reset(); // Act @@ -234,14 +176,13 @@ public async Task DocumentAdded_NullLoader_HasEmptyText() public async Task DocumentAdded_WithLoader_LoadesText() { // Arrange - ProjectManager.HostProjectAdded(HostProject); - ProjectManager.WorkspaceProjectAdded(WorkspaceProject); + ProjectManager.ProjectAdded(HostProject); ProjectManager.Reset(); var expected = SourceText.From("Hello"); // Act - ProjectManager.DocumentAdded(HostProject, Documents[0], TextLoader.From(TextAndVersion.Create(expected,VersionStamp.Default))); + ProjectManager.DocumentAdded(HostProject, Documents[0], TextLoader.From(TextAndVersion.Create(expected, VersionStamp.Default))); // Assert var snapshot = ProjectManager.GetSnapshot(HostProject); @@ -252,31 +193,28 @@ public async Task DocumentAdded_WithLoader_LoadesText() } [ForegroundFact] - public async Task DocumentAdded_CachesTagHelpers() + public void DocumentAdded_CachesTagHelpers() { // Arrange - ProjectManager.HostProjectAdded(HostProject); - ProjectManager.WorkspaceProjectAdded(WorkspaceProject); + ProjectManager.ProjectAdded(HostProject); + ProjectManager.ProjectWorkspaceStateChanged(HostProject.FilePath, ProjectWorkspaceStateWithTagHelpers); ProjectManager.Reset(); - - // Adding some computed state - var snapshot = ProjectManager.GetSnapshot(HostProject); - await snapshot.GetTagHelpersAsync(); + var originalTagHelpers = ProjectManager.GetSnapshot(HostProject).TagHelpers; // Act ProjectManager.DocumentAdded(HostProject, Documents[0], null); // Assert - snapshot = ProjectManager.GetSnapshot(HostProject); - Assert.True(snapshot.TryGetTagHelpers(out var _)); + var newTagHelpers = ProjectManager.GetSnapshot(HostProject).TagHelpers; + Assert.Same(originalTagHelpers, newTagHelpers); } [ForegroundFact] public void DocumentAdded_CachesProjectEngine() { // Arrange - ProjectManager.HostProjectAdded(HostProject); + ProjectManager.ProjectAdded(HostProject); ProjectManager.Reset(); var snapshot = ProjectManager.GetSnapshot(HostProject); @@ -294,8 +232,7 @@ public void DocumentAdded_CachesProjectEngine() public void DocumentRemoved_RemovesDocument() { // Arrange - ProjectManager.HostProjectAdded(HostProject); - ProjectManager.WorkspaceProjectAdded(WorkspaceProject); + ProjectManager.ProjectAdded(HostProject); ProjectManager.DocumentAdded(HostProject, Documents[0], null); ProjectManager.DocumentAdded(HostProject, Documents[1], null); ProjectManager.DocumentAdded(HostProject, Documents[2], null); @@ -307,7 +244,7 @@ public void DocumentRemoved_RemovesDocument() // Assert var snapshot = ProjectManager.GetSnapshot(HostProject); Assert.Collection( - snapshot.DocumentFilePaths.OrderBy(f => f), + snapshot.DocumentFilePaths.OrderBy(f => f), d => Assert.Equal(Documents[2].FilePath, d), d => Assert.Equal(Documents[0].FilePath, d)); @@ -318,8 +255,7 @@ public void DocumentRemoved_RemovesDocument() public void DocumentRemoved_IgnoresNotFoundDocument() { // Arrange - ProjectManager.HostProjectAdded(HostProject); - ProjectManager.WorkspaceProjectAdded(WorkspaceProject); + ProjectManager.ProjectAdded(HostProject); ProjectManager.Reset(); // Act @@ -346,34 +282,31 @@ public void DocumentRemoved_IgnoresUnknownProject() } [ForegroundFact] - public async Task DocumentRemoved_CachesTagHelpers() + public void DocumentRemoved_CachesTagHelpers() { // Arrange - ProjectManager.HostProjectAdded(HostProject); - ProjectManager.WorkspaceProjectAdded(WorkspaceProject); + ProjectManager.ProjectAdded(HostProject); + ProjectManager.ProjectWorkspaceStateChanged(HostProject.FilePath, ProjectWorkspaceStateWithTagHelpers); ProjectManager.DocumentAdded(HostProject, Documents[0], null); ProjectManager.DocumentAdded(HostProject, Documents[1], null); ProjectManager.DocumentAdded(HostProject, Documents[2], null); ProjectManager.Reset(); - // Adding some computed state - var snapshot = ProjectManager.GetSnapshot(HostProject); - await snapshot.GetTagHelpersAsync(); + var originalTagHelpers = ProjectManager.GetSnapshot(HostProject).TagHelpers; // Act ProjectManager.DocumentRemoved(HostProject, Documents[1]); // Assert - snapshot = ProjectManager.GetSnapshot(HostProject); - Assert.True(snapshot.TryGetTagHelpers(out var _)); + var newTagHelpers = ProjectManager.GetSnapshot(HostProject).TagHelpers; + Assert.Same(originalTagHelpers, newTagHelpers); } [ForegroundFact] public void DocumentRemoved_CachesProjectEngine() { // Arrange - ProjectManager.HostProjectAdded(HostProject); - ProjectManager.WorkspaceProjectAdded(WorkspaceProject); + ProjectManager.ProjectAdded(HostProject); ProjectManager.DocumentAdded(HostProject, Documents[0], null); ProjectManager.DocumentAdded(HostProject, Documents[1], null); ProjectManager.DocumentAdded(HostProject, Documents[2], null); @@ -394,8 +327,7 @@ public void DocumentRemoved_CachesProjectEngine() public async Task DocumentOpened_UpdatesDocument() { // Arrange - ProjectManager.HostProjectAdded(HostProject); - ProjectManager.WorkspaceProjectAdded(WorkspaceProject); + ProjectManager.ProjectAdded(HostProject); ProjectManager.DocumentAdded(HostProject, Documents[0], null); ProjectManager.Reset(); @@ -416,8 +348,7 @@ public async Task DocumentOpened_UpdatesDocument() public async Task DocumentClosed_UpdatesDocument() { // Arrange - ProjectManager.HostProjectAdded(HostProject); - ProjectManager.WorkspaceProjectAdded(WorkspaceProject); + ProjectManager.ProjectAdded(HostProject); ProjectManager.DocumentAdded(HostProject, Documents[0], null); ProjectManager.DocumentOpened(HostProject.FilePath, Documents[0].FilePath, SourceText); ProjectManager.Reset(); @@ -438,14 +369,13 @@ public async Task DocumentClosed_UpdatesDocument() Assert.Same(expected, text); Assert.False(ProjectManager.IsDocumentOpen(Documents[0].FilePath)); } - + [ForegroundFact] public async Task DocumentClosed_AcceptsChange() { // Arrange - ProjectManager.HostProjectAdded(HostProject); - ProjectManager.WorkspaceProjectAdded(WorkspaceProject); + ProjectManager.ProjectAdded(HostProject); ProjectManager.DocumentAdded(HostProject, Documents[0], null); ProjectManager.Reset(); @@ -467,8 +397,7 @@ public async Task DocumentClosed_AcceptsChange() public async Task DocumentChanged_Snapshot_UpdatesDocument() { // Arrange - ProjectManager.HostProjectAdded(HostProject); - ProjectManager.WorkspaceProjectAdded(WorkspaceProject); + ProjectManager.ProjectAdded(HostProject); ProjectManager.DocumentAdded(HostProject, Documents[0], null); ProjectManager.DocumentOpened(HostProject.FilePath, Documents[0].FilePath, SourceText); ProjectManager.Reset(); @@ -490,8 +419,7 @@ public async Task DocumentChanged_Snapshot_UpdatesDocument() public async Task DocumentChanged_Loader_UpdatesDocument() { // Arrange - ProjectManager.HostProjectAdded(HostProject); - ProjectManager.WorkspaceProjectAdded(WorkspaceProject); + ProjectManager.ProjectAdded(HostProject); ProjectManager.DocumentAdded(HostProject, Documents[0], null); ProjectManager.DocumentOpened(HostProject.FilePath, Documents[0].FilePath, SourceText); ProjectManager.Reset(); @@ -511,84 +439,59 @@ public async Task DocumentChanged_Loader_UpdatesDocument() } [ForegroundFact] - public void HostProjectAdded_WithoutWorkspaceProject_NotifiesListeners() - { - // Arrange - - // Act - ProjectManager.HostProjectAdded(HostProject); - - // Assert - var snapshot = ProjectManager.GetSnapshot(HostProject); - Assert.False(snapshot.IsInitialized); - - Assert.Equal(ProjectChangeKind.ProjectAdded, ProjectManager.ListenersNotifiedOf); - } - - [ForegroundFact] - public void HostProjectAdded_FindsWorkspaceProject_NotifiesListeners() + public void ProjectAdded_WithoutWorkspaceProject_NotifiesListeners() { // Arrange - Assert.True(Workspace.TryApplyChanges(WorkspaceProject.Solution)); // Act - ProjectManager.HostProjectAdded(HostProject); + ProjectManager.ProjectAdded(HostProject); // Assert - var snapshot = ProjectManager.GetSnapshot(HostProject); - Assert.True(snapshot.IsInitialized); - Assert.Equal(ProjectChangeKind.ProjectAdded, ProjectManager.ListenersNotifiedOf); } [ForegroundFact] - public void HostProjectChanged_ConfigurationChange_WithoutWorkspaceProject_NotifiesListeners() + public void ProjectConfigurationChanged_ConfigurationChange_ProjectWorkspaceState_NotifiesListeners() { // Arrange - ProjectManager.HostProjectAdded(HostProject); + ProjectManager.ProjectAdded(HostProject); ProjectManager.Reset(); // Act - ProjectManager.HostProjectChanged(HostProjectWithConfigurationChange); + ProjectManager.ProjectConfigurationChanged(HostProjectWithConfigurationChange); // Assert var snapshot = ProjectManager.GetSnapshot(HostProjectWithConfigurationChange); - Assert.False(snapshot.IsInitialized); - Assert.Equal(ProjectChangeKind.ProjectChanged, ProjectManager.ListenersNotifiedOf); } [ForegroundFact] - public void HostProjectChanged_ConfigurationChange_WithWorkspaceProject_NotifiesListeners() + public void ProjectConfigurationChanged_ConfigurationChange_WithProjectWorkspaceState_NotifiesListeners() { // Arrange - ProjectManager.HostProjectAdded(HostProject); - ProjectManager.WorkspaceProjectAdded(WorkspaceProject); + ProjectManager.ProjectAdded(HostProject); + ProjectManager.ProjectWorkspaceStateChanged(HostProject.FilePath, ProjectWorkspaceStateWithTagHelpers); ProjectManager.Reset(); // Act - ProjectManager.HostProjectChanged(HostProjectWithConfigurationChange); + ProjectManager.ProjectConfigurationChanged(HostProjectWithConfigurationChange); // Assert - var snapshot = ProjectManager.GetSnapshot(HostProjectWithConfigurationChange); - Assert.True(snapshot.IsInitialized); - Assert.Equal(ProjectChangeKind.ProjectChanged, ProjectManager.ListenersNotifiedOf); } [ForegroundFact] - public void HostProjectChanged_ConfigurationChange_DoesNotCacheProjectEngine() + public void ProjectConfigurationChanged_ConfigurationChange_DoesNotCacheProjectEngine() { // Arrange - ProjectManager.HostProjectAdded(HostProject); - ProjectManager.WorkspaceProjectAdded(WorkspaceProject); + ProjectManager.ProjectAdded(HostProject); ProjectManager.Reset(); var snapshot = ProjectManager.GetSnapshot(HostProject); var projectEngine = snapshot.GetProjectEngine(); // Act - ProjectManager.HostProjectChanged(HostProjectWithConfigurationChange); + ProjectManager.ProjectConfigurationChanged(HostProjectWithConfigurationChange); // Assert snapshot = ProjectManager.GetSnapshot(HostProjectWithConfigurationChange); @@ -596,34 +499,12 @@ public void HostProjectChanged_ConfigurationChange_DoesNotCacheProjectEngine() } [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] - public void HostProjectChanged_IgnoresUnknownProject() + public void ProjectConfigurationChanged_IgnoresUnknownProject() { // Arrange // Act - ProjectManager.HostProjectChanged(HostProject); + ProjectManager.ProjectConfigurationChanged(HostProject); // Assert Assert.Empty(ProjectManager.Projects); @@ -632,14 +513,14 @@ public void HostProjectChanged_IgnoresUnknownProject() } [ForegroundFact] - public void HostProjectRemoved_RemovesProject_NotifiesListeners() + public void ProjectRemoved_RemovesProject_NotifiesListeners() { // Arrange - ProjectManager.HostProjectAdded(HostProject); + ProjectManager.ProjectAdded(HostProject); ProjectManager.Reset(); // Act - ProjectManager.HostProjectRemoved(HostProject); + ProjectManager.ProjectRemoved(HostProject); // Assert Assert.Empty(ProjectManager.Projects); @@ -648,12 +529,12 @@ public void HostProjectRemoved_RemovesProject_NotifiesListeners() } [ForegroundFact] - public void WorkspaceProjectAdded_WithoutHostProject_IgnoresWorkspaceProject() + public void ProjectWorkspaceStateChanged_WithoutHostProject_IgnoresWorkspaceState() { // Arrange // Act - ProjectManager.WorkspaceProjectAdded(WorkspaceProject); + ProjectManager.ProjectWorkspaceStateChanged(HostProject.FilePath, ProjectWorkspaceStateWithTagHelpers); // Assert Assert.Empty(ProjectManager.Projects); @@ -662,71 +543,16 @@ public void WorkspaceProjectAdded_WithoutHostProject_IgnoresWorkspaceProject() } [ForegroundFact] - public void WorkspaceProjectAdded_IgnoresNonCSharpProject() - { - // Arrange - ProjectManager.HostProjectAdded(HostProject); - ProjectManager.Reset(); - - // Act - ProjectManager.WorkspaceProjectAdded(VBWorkspaceProject); - - // Assert - var snapshot = ProjectManager.GetSnapshot(WorkspaceProject); - Assert.False(snapshot.IsInitialized); - - Assert.Null(ProjectManager.ListenersNotifiedOf); - } - - [ForegroundFact] - public void WorkspaceProjectAdded_IgnoresSecondProjectWithSameFilePath() - { - // Arrange - ProjectManager.HostProjectAdded(HostProject); - ProjectManager.WorkspaceProjectAdded(WorkspaceProject); - ProjectManager.Reset(); - - // Act - ProjectManager.WorkspaceProjectAdded(WorkspaceProjectWithDifferentTfm); - - // Assert - var snapshot = ProjectManager.GetSnapshot(WorkspaceProject); - Assert.Same(WorkspaceProject, snapshot.WorkspaceProject); - - Assert.Null(ProjectManager.ListenersNotifiedOf); - } - - [ForegroundFact] - public void WorkspaceProjectAdded_IgnoresProjectWithoutFilePath() - { - // Arrange - ProjectManager.HostProjectAdded(HostProject); - ProjectManager.Reset(); - - // Act - ProjectManager.WorkspaceProjectAdded(WorkspaceProjectWithoutFilePath); - - // Assert - var snapshot = ProjectManager.GetSnapshot(WorkspaceProject); - Assert.False(snapshot.IsInitialized); - - Assert.Null(ProjectManager.ListenersNotifiedOf); - } - - [ForegroundFact] - public void WorkspaceProjectAdded_WithHostProject_NotifiesListenters() + public void ProjectWorkspaceStateChanged_WithHostProject_FirstTime_NotifiesListenters() { // Arrange - ProjectManager.HostProjectAdded(HostProject); + ProjectManager.ProjectAdded(HostProject); ProjectManager.Reset(); // Act - ProjectManager.WorkspaceProjectAdded(WorkspaceProject); + ProjectManager.ProjectWorkspaceStateChanged(HostProject.FilePath, ProjectWorkspaceStateWithTagHelpers); // Assert - var snapshot = ProjectManager.GetSnapshot(WorkspaceProject); - Assert.True(snapshot.IsInitialized); - Assert.Equal(ProjectChangeKind.ProjectChanged, ProjectManager.ListenersNotifiedOf); } @@ -734,255 +560,17 @@ public void WorkspaceProjectAdded_WithHostProject_NotifiesListenters() 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); - } - - // We always update the snapshot when someone calls WorkspaceProjectChanged. This is how we deal - // with changes to source code, which wouldn't result in a new project. - [ForegroundFact] - public void WorkspaceProjectChanged_WithHostProject_NotifiesListeners() - { - // Arrange - ProjectManager.HostProjectAdded(HostProject); - ProjectManager.WorkspaceProjectAdded(WorkspaceProject); + ProjectManager.ProjectAdded(HostProject); + ProjectManager.ProjectWorkspaceStateChanged(HostProject.FilePath, ProjectWorkspaceState.Default); ProjectManager.Reset(); // Act - ProjectManager.WorkspaceProjectChanged(WorkspaceProject); + ProjectManager.ProjectWorkspaceStateChanged(HostProject.FilePath, ProjectWorkspaceStateWithTagHelpers); // Assert - var snapshot = ProjectManager.GetSnapshot(WorkspaceProject); - Assert.True(snapshot.IsInitialized); - Assert.Equal(ProjectChangeKind.ProjectChanged, ProjectManager.ListenersNotifiedOf); } - [ForegroundFact] - public void WorkspaceProjectChanged_WithHostProject_CanNoOpForSecondProject() - { - // Arrange - ProjectManager.HostProjectAdded(HostProject); - ProjectManager.WorkspaceProjectAdded(WorkspaceProject); - ProjectManager.Reset(); - - // Act - ProjectManager.WorkspaceProjectChanged(WorkspaceProjectWithDifferentTfm); - - // Assert - var snapshot = ProjectManager.GetSnapshot(WorkspaceProject); - Assert.True(snapshot.IsInitialized); - - Assert.Null(ProjectManager.ListenersNotifiedOf); - } - - [ForegroundFact] - public void WorkspaceProjectChanged_WithoutHostProject_IgnoresWorkspaceProject() - { - // Arrange - ProjectManager.WorkspaceProjectAdded(WorkspaceProject); - ProjectManager.Reset(); - - var project = WorkspaceProject.WithAssemblyName("Test1"); // Simulate a project change - - // Act - ProjectManager.WorkspaceProjectChanged(project); - - // Assert - Assert.Empty(ProjectManager.Projects); - - Assert.Null(ProjectManager.ListenersNotifiedOf); - } - - [ForegroundFact] - public void WorkspaceProjectChanged_IgnoresNonCSharpProject() - { - // Arrange - ProjectManager.HostProjectAdded(HostProject); - ProjectManager.WorkspaceProjectAdded(VBWorkspaceProject); - ProjectManager.Reset(); - - var project = VBWorkspaceProject.WithAssemblyName("Test1"); // Simulate a project change - - // Act - ProjectManager.WorkspaceProjectChanged(project); - - // Assert - var snapshot = ProjectManager.GetSnapshot(WorkspaceProject); - Assert.False(snapshot.IsInitialized); - - Assert.Null(ProjectManager.ListenersNotifiedOf); - } - - [ForegroundFact] - public void WorkspaceProjectChanged_IgnoresProjectWithoutFilePath() - { - // Arrange - ProjectManager.HostProjectAdded(HostProject); - ProjectManager.WorkspaceProjectAdded(WorkspaceProjectWithoutFilePath); - ProjectManager.Reset(); - - var project = WorkspaceProjectWithoutFilePath.WithAssemblyName("Test1"); // Simulate a project change - - // Act - ProjectManager.WorkspaceProjectChanged(project); - - // Assert - var snapshot = ProjectManager.GetSnapshot(WorkspaceProject); - Assert.False(snapshot.IsInitialized); - - Assert.Null(ProjectManager.ListenersNotifiedOf); - } - - [ForegroundFact] - public void WorkspaceProjectChanged_IgnoresSecondProjectWithSameFilePath() - { - // Arrange - ProjectManager.HostProjectAdded(HostProject); - ProjectManager.WorkspaceProjectAdded(WorkspaceProject); - ProjectManager.Reset(); - - // Act - ProjectManager.WorkspaceProjectChanged(WorkspaceProjectWithDifferentTfm); - - // Assert - var snapshot = ProjectManager.GetSnapshot(WorkspaceProject); - Assert.Same(WorkspaceProject, snapshot.WorkspaceProject); - - Assert.Null(ProjectManager.ListenersNotifiedOf); - } - - [ForegroundFact] - public async Task WorkspaceProjectRemoved_DoesNotRemoveProject_RemovesTagHelpers() - { - // Arrange - ProjectManager.HostProjectAdded(HostProject); - ProjectManager.WorkspaceProjectAdded(WorkspaceProject); - ProjectManager.Reset(); - - var snapshot = ProjectManager.GetSnapshot(HostProject); - - // Adding some computed state - await snapshot.GetTagHelpersAsync(); - - // Act - ProjectManager.WorkspaceProjectRemoved(WorkspaceProject); - - // Assert - snapshot = ProjectManager.GetSnapshot(WorkspaceProject); - Assert.False(snapshot.IsInitialized); - Assert.False(snapshot.TryGetTagHelpers(out var _)); - - Assert.Equal(ProjectChangeKind.ProjectChanged, ProjectManager.ListenersNotifiedOf); - } - - [ForegroundFact] - public async Task WorkspaceProjectRemoved_FallsBackToSecondProject() - { - // Arrange - ProjectManager.HostProjectAdded(HostProject); - ProjectManager.WorkspaceProjectAdded(WorkspaceProject); - ProjectManager.Reset(); - - var snapshot = ProjectManager.GetSnapshot(HostProject); - - // Adding some computed state - await snapshot.GetTagHelpersAsync(); - - // Sets up a solution where the which has WorkspaceProjectWithDifferentTfm but not WorkspaceProject - // This will enable us to fall back and find the WorkspaceProjectWithDifferentTfm - Assert.True(Workspace.TryApplyChanges(WorkspaceProjectWithDifferentTfm.Solution)); - - // Act - ProjectManager.WorkspaceProjectRemoved(WorkspaceProject); - - // Assert - snapshot = ProjectManager.GetSnapshot(WorkspaceProject); - Assert.True(snapshot.IsInitialized); - Assert.Equal(WorkspaceProjectWithDifferentTfm.Id, snapshot.WorkspaceProject.Id); - Assert.False(snapshot.TryGetTagHelpers(out var _)); - - Assert.Equal(ProjectChangeKind.ProjectChanged, ProjectManager.ListenersNotifiedOf); - } - - [ForegroundFact] - public void WorkspaceProjectRemoved_IgnoresSecondProjectWithSameFilePath() - { - // Arrange - ProjectManager.HostProjectAdded(HostProject); - ProjectManager.WorkspaceProjectAdded(WorkspaceProject); - ProjectManager.Reset(); - - // Act - ProjectManager.WorkspaceProjectRemoved(WorkspaceProjectWithDifferentTfm); - - // Assert - var snapshot = ProjectManager.GetSnapshot(WorkspaceProject); - Assert.Same(WorkspaceProject, snapshot.WorkspaceProject); - - Assert.Null(ProjectManager.ListenersNotifiedOf); - } - - [ForegroundFact] - public void WorkspaceProjectRemoved_IgnoresNonCSharpProject() - { - // Arrange - ProjectManager.HostProjectAdded(HostProject); - ProjectManager.WorkspaceProjectAdded(VBWorkspaceProject); - ProjectManager.Reset(); - - // Act - ProjectManager.WorkspaceProjectRemoved(VBWorkspaceProject); - - // Assert - var snapshot = ProjectManager.GetSnapshot(WorkspaceProject); - Assert.False(snapshot.IsInitialized); - - Assert.Null(ProjectManager.ListenersNotifiedOf); - } - - [ForegroundFact] - public void WorkspaceProjectRemoved_IgnoresProjectWithoutFilePath() - { - // Arrange - ProjectManager.HostProjectAdded(HostProject); - ProjectManager.WorkspaceProjectAdded(WorkspaceProjectWithoutFilePath); - ProjectManager.Reset(); - - // Act - ProjectManager.WorkspaceProjectRemoved(WorkspaceProjectWithoutFilePath); - - // Assert - var snapshot = ProjectManager.GetSnapshot(WorkspaceProject); - Assert.False(snapshot.IsInitialized); - - Assert.Null(ProjectManager.ListenersNotifiedOf); - } - - [ForegroundFact] - public void WorkspaceProjectRemoved_IgnoresUnknownProject() - { - // Arrange - - // Act - ProjectManager.WorkspaceProjectRemoved(WorkspaceProject); - - // Assert - Assert.Empty(ProjectManager.Projects); - - Assert.Null(ProjectManager.ListenersNotifiedOf); - } - private class TestProjectSnapshotManager : DefaultProjectSnapshotManager { public TestProjectSnapshotManager(ForegroundDispatcher dispatcher, IEnumerable triggers, Workspace workspace) diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/ProjectSystem/TestProjectWorkspaceStateGenerator.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/ProjectSystem/TestProjectWorkspaceStateGenerator.cs new file mode 100644 index 00000000000..6e12c0bfec6 --- /dev/null +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/ProjectSystem/TestProjectWorkspaceStateGenerator.cs @@ -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.Collections.Generic; +using Microsoft.CodeAnalysis.Razor; +using Microsoft.CodeAnalysis.Razor.ProjectSystem; +using Microsoft.CodeAnalysis; + +namespace Microsoft.VisualStudio.LanguageServices.Razor.Test +{ + internal class TestProjectWorkspaceStateGenerator : ProjectWorkspaceStateGenerator + { + private List<(Project workspaceProject, ProjectSnapshot projectSnapshot)> _updates; + + public TestProjectWorkspaceStateGenerator() + { + _updates = new List<(Project workspaceProject, ProjectSnapshot projectSnapshot)>(); + } + + public IReadOnlyList<(Project workspaceProject, ProjectSnapshot projectSnapshot)> UpdateQueue => _updates; + + public override void Initialize(ProjectSnapshotManagerBase projectManager) + { + } + + public override void Update(Project workspaceProject, ProjectSnapshot projectSnapshot) + { + var update = (workspaceProject, projectSnapshot); + _updates.Add(update); + } + } +} diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/ProjectSystem/WorkspaceProjectSnapshotChangeTriggerTest.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/ProjectSystem/WorkspaceProjectStateChangeDetectorTest.cs similarity index 58% rename from src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/ProjectSystem/WorkspaceProjectSnapshotChangeTriggerTest.cs rename to src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/ProjectSystem/WorkspaceProjectStateChangeDetectorTest.cs index f5931269d25..87429567bf6 100644 --- a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/ProjectSystem/WorkspaceProjectSnapshotChangeTriggerTest.cs +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/ProjectSystem/WorkspaceProjectStateChangeDetectorTest.cs @@ -4,14 +4,15 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using Microsoft.VisualStudio.LanguageServices.Razor.Test; using Moq; using Xunit; namespace Microsoft.CodeAnalysis.Razor.ProjectSystem { - public class WorkspaceProjectSnapshotChangeTriggerTest : ForegroundDispatcherWorkspaceTestBase + public class WorkspaceProjectStateChangeDetectorTest : ForegroundDispatcherWorkspaceTestBase { - public WorkspaceProjectSnapshotChangeTriggerTest() + public WorkspaceProjectStateChangeDetectorTest() { EmptySolution = Workspace.CurrentSolution.GetIsolatedSolution(); @@ -77,24 +78,25 @@ public WorkspaceProjectSnapshotChangeTriggerTest() [InlineData(WorkspaceChangeKind.SolutionCleared)] [InlineData(WorkspaceChangeKind.SolutionReloaded)] [InlineData(WorkspaceChangeKind.SolutionRemoved)] - public void WorkspaceChanged_SolutionEvents_AddsProjectsInSolution(WorkspaceChangeKind kind) + public void WorkspaceChanged_SolutionEvents_EnqueuesUpdatesForProjectsInSolution(WorkspaceChangeKind kind) { // Arrange - var trigger = new WorkspaceProjectSnapshotChangeTrigger(); - var projectManager = new TestProjectSnapshotManager(new[] { trigger }, Workspace); - projectManager.HostProjectAdded(HostProjectOne); - projectManager.HostProjectAdded(HostProjectTwo); + var workspaceStateGenerator = new TestProjectWorkspaceStateGenerator(); + var detector = new WorkspaceProjectStateChangeDetector(workspaceStateGenerator); + var projectManager = new TestProjectSnapshotManager(new[] { detector }, Workspace); + projectManager.ProjectAdded(HostProjectOne); + projectManager.ProjectAdded(HostProjectTwo); var e = new WorkspaceChangeEventArgs(kind, oldSolution: EmptySolution, newSolution: SolutionWithTwoProjects); // Act - trigger.Workspace_WorkspaceChanged(Workspace, e); + detector.Workspace_WorkspaceChanged(Workspace, e); // Assert Assert.Collection( - projectManager.Projects.OrderBy(p => p.WorkspaceProject.Name), - p => Assert.Equal(ProjectNumberOne.Id, p.WorkspaceProject.Id), - p => Assert.Equal(ProjectNumberTwo.Id, p.WorkspaceProject.Id)); + workspaceStateGenerator.UpdateQueue, + p => Assert.Equal(ProjectNumberOne.Id, p.workspaceProject.Id), + p => Assert.Equal(ProjectNumberTwo.Id, p.workspaceProject.Id)); } [ForegroundTheory] @@ -103,118 +105,108 @@ public void WorkspaceChanged_SolutionEvents_AddsProjectsInSolution(WorkspaceChan [InlineData(WorkspaceChangeKind.SolutionCleared)] [InlineData(WorkspaceChangeKind.SolutionReloaded)] [InlineData(WorkspaceChangeKind.SolutionRemoved)] - public void WorkspaceChanged_SolutionEvents_ClearsExistingProjects_AddsProjectsInSolution(WorkspaceChangeKind kind) + public void WorkspaceChanged_SolutionEvents_EnqueuesStateClear_EnqueuesSolutionProjectUpdates(WorkspaceChangeKind kind) { // Arrange - var trigger = new WorkspaceProjectSnapshotChangeTrigger(); - var projectManager = new TestProjectSnapshotManager(new[] { trigger }, Workspace); - projectManager.HostProjectAdded(HostProjectOne); - projectManager.HostProjectAdded(HostProjectTwo); - projectManager.HostProjectAdded(HostProjectThree); + var workspaceStateGenerator = new TestProjectWorkspaceStateGenerator(); + var detector = new WorkspaceProjectStateChangeDetector(workspaceStateGenerator); + var projectManager = new TestProjectSnapshotManager(new[] { detector }, Workspace); + projectManager.ProjectAdded(HostProjectOne); + projectManager.ProjectAdded(HostProjectTwo); + projectManager.ProjectAdded(HostProjectThree); // Initialize with a project. This will get removed. var e = new WorkspaceChangeEventArgs(WorkspaceChangeKind.SolutionAdded, oldSolution: EmptySolution, newSolution: SolutionWithOneProject); - trigger.Workspace_WorkspaceChanged(Workspace, e); + detector.Workspace_WorkspaceChanged(Workspace, e); e = new WorkspaceChangeEventArgs(kind, oldSolution: SolutionWithOneProject, newSolution: SolutionWithTwoProjects); // Act - trigger.Workspace_WorkspaceChanged(Workspace, e); + detector.Workspace_WorkspaceChanged(Workspace, e); // Assert Assert.Collection( - projectManager.Projects.OrderBy(p => p.WorkspaceProject?.Name), - p => Assert.Null(p.WorkspaceProject), - p => Assert.Equal(ProjectNumberOne.Id, p.WorkspaceProject.Id), - p => Assert.Equal(ProjectNumberTwo.Id, p.WorkspaceProject.Id)); + workspaceStateGenerator.UpdateQueue, + p => Assert.Equal(ProjectNumberThree.Id, p.workspaceProject.Id), + p => Assert.Null(p.workspaceProject), + p => Assert.Equal(ProjectNumberOne.Id, p.workspaceProject.Id), + p => Assert.Equal(ProjectNumberTwo.Id, p.workspaceProject.Id)); } [ForegroundTheory] [InlineData(WorkspaceChangeKind.ProjectChanged)] [InlineData(WorkspaceChangeKind.ProjectReloaded)] - public async Task WorkspaceChanged_ProjectChangeEvents_UpdatesProject_AfterDelay(WorkspaceChangeKind kind) + public async Task WorkspaceChanged_ProjectChangeEvents_UpdatesProjectState_AfterDelay(WorkspaceChangeKind kind) { // Arrange - var trigger = new WorkspaceProjectSnapshotChangeTrigger() + var workspaceStateGenerator = new TestProjectWorkspaceStateGenerator(); + var detector = new WorkspaceProjectStateChangeDetector(workspaceStateGenerator) { - ProjectChangeDelay = 50, + EnqueueDelay = 50, }; - var projectManager = new TestProjectSnapshotManager(new[] { trigger }, Workspace); - projectManager.HostProjectAdded(HostProjectOne); - projectManager.HostProjectAdded(HostProjectTwo); - - // Initialize with some projects. - var e = new WorkspaceChangeEventArgs(WorkspaceChangeKind.SolutionAdded, oldSolution: EmptySolution, newSolution: SolutionWithTwoProjects); - trigger.Workspace_WorkspaceChanged(Workspace, e); + var projectManager = new TestProjectSnapshotManager(new[] { detector }, Workspace); + projectManager.ProjectAdded(HostProjectOne); var solution = SolutionWithTwoProjects.WithProjectAssemblyName(ProjectNumberOne.Id, "Changed"); - e = new WorkspaceChangeEventArgs(kind, oldSolution: SolutionWithTwoProjects, newSolution: solution, projectId: ProjectNumberOne.Id); + var e = new WorkspaceChangeEventArgs(kind, oldSolution: SolutionWithTwoProjects, newSolution: solution, projectId: ProjectNumberOne.Id); // Act - trigger.Workspace_WorkspaceChanged(Workspace, e); + detector.Workspace_WorkspaceChanged(Workspace, e); // Assert // // The change hasn't come through yet. - Assert.Equal("One", projectManager.Projects.Single().WorkspaceProject.AssemblyName); + Assert.Empty(workspaceStateGenerator.UpdateQueue); - await trigger._deferredUpdates.Single().Value; + await detector._deferredUpdates.Single().Value; - Assert.Collection( - projectManager.Projects.OrderBy(p => p.WorkspaceProject.Name), - p => - { - Assert.Equal(ProjectNumberOne.Id, p.WorkspaceProject.Id); - Assert.Equal("Changed", p.WorkspaceProject.AssemblyName); - }, - p => Assert.Equal(ProjectNumberTwo.Id, p.WorkspaceProject.Id)); + var update = Assert.Single(workspaceStateGenerator.UpdateQueue); + Assert.Equal(update.workspaceProject.Id, ProjectNumberOne.Id); + Assert.Equal(update.projectSnapshot.FilePath, HostProjectOne.FilePath); } [ForegroundFact] - public void WorkspaceChanged_ProjectRemovedEvent_RemovesProject() + public void WorkspaceChanged_ProjectRemovedEvent_QueuesProjectStateRemoval() { // Arrange - var trigger = new WorkspaceProjectSnapshotChangeTrigger(); - var projectManager = new TestProjectSnapshotManager(new[] { trigger }, Workspace); - projectManager.HostProjectAdded(HostProjectOne); - projectManager.HostProjectAdded(HostProjectTwo); - - // Initialize with some projects project. - var e = new WorkspaceChangeEventArgs(WorkspaceChangeKind.SolutionAdded, oldSolution: EmptySolution, newSolution: SolutionWithTwoProjects); - trigger.Workspace_WorkspaceChanged(Workspace, e); + var workspaceStateGenerator = new TestProjectWorkspaceStateGenerator(); + var detector = new WorkspaceProjectStateChangeDetector(workspaceStateGenerator); + var projectManager = new TestProjectSnapshotManager(new[] { detector }, Workspace); + projectManager.ProjectAdded(HostProjectOne); + projectManager.ProjectAdded(HostProjectTwo); var solution = SolutionWithTwoProjects.RemoveProject(ProjectNumberOne.Id); - e = new WorkspaceChangeEventArgs(WorkspaceChangeKind.ProjectRemoved, oldSolution: SolutionWithTwoProjects, newSolution: solution, projectId: ProjectNumberOne.Id); + var e = new WorkspaceChangeEventArgs(WorkspaceChangeKind.ProjectRemoved, oldSolution: SolutionWithTwoProjects, newSolution: solution, projectId: ProjectNumberOne.Id); // Act - trigger.Workspace_WorkspaceChanged(Workspace, e); + detector.Workspace_WorkspaceChanged(Workspace, e); // Assert Assert.Collection( - projectManager.Projects.OrderBy(p => p.WorkspaceProject?.Name), - p => Assert.Null(p.WorkspaceProject), - p => Assert.Equal(ProjectNumberTwo.Id, p.WorkspaceProject.Id)); + workspaceStateGenerator.UpdateQueue, + p => Assert.Null(p.workspaceProject)); } [ForegroundFact] public void WorkspaceChanged_ProjectAddedEvent_AddsProject() { // Arrange - var trigger = new WorkspaceProjectSnapshotChangeTrigger(); - var projectManager = new TestProjectSnapshotManager(new[] { trigger }, Workspace); - projectManager.HostProjectAdded(HostProjectThree); + var workspaceStateGenerator = new TestProjectWorkspaceStateGenerator(); + var detector = new WorkspaceProjectStateChangeDetector(workspaceStateGenerator); + var projectManager = new TestProjectSnapshotManager(new[] { detector }, Workspace); + projectManager.ProjectAdded(HostProjectThree); var solution = SolutionWithOneProject; var e = new WorkspaceChangeEventArgs(WorkspaceChangeKind.ProjectAdded, oldSolution: EmptySolution, newSolution: solution, projectId: ProjectNumberThree.Id); // Act - trigger.Workspace_WorkspaceChanged(Workspace, e); + detector.Workspace_WorkspaceChanged(Workspace, e); // Assert Assert.Collection( - projectManager.Projects.OrderBy(p => p.WorkspaceProject.Name), - p => Assert.Equal(ProjectNumberThree.Id, p.WorkspaceProject.Id)); + workspaceStateGenerator.UpdateQueue, + p => Assert.Equal(ProjectNumberThree.Id, p.workspaceProject.Id)); } private class TestProjectSnapshotManager : DefaultProjectSnapshotManager diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Serialization/ProjectSnapshotHandleSerializationTest.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Serialization/ProjectSnapshotHandleSerializationTest.cs index a569676b208..3eeb03567a6 100644 --- a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Serialization/ProjectSnapshotHandleSerializationTest.cs +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Serialization/ProjectSnapshotHandleSerializationTest.cs @@ -1,10 +1,8 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using System; using System.Linq; using Microsoft.AspNetCore.Razor.Language; -using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Razor; using Microsoft.CodeAnalysis.Razor.ProjectSystem; using Newtonsoft.Json; @@ -30,14 +28,13 @@ public void ProjectSnapshotHandleJsonConverter_Serialization_CanKindaRoundTrip() var snapshot = new ProjectSnapshotHandle( "Test.csproj", new ProjectSystemRazorConfiguration( - RazorLanguageVersion.Version_1_1, + RazorLanguageVersion.Version_1_1, "Test", new[] { new ProjectSystemRazorExtension("Test-Extension1"), new ProjectSystemRazorExtension("Test-Extension2"), - }), - ProjectId.CreateFromSerialized(Guid.NewGuid(), "Test")); + })); // Act var json = JsonConvert.SerializeObject(snapshot, Converters); @@ -47,18 +44,17 @@ public void ProjectSnapshotHandleJsonConverter_Serialization_CanKindaRoundTrip() Assert.Equal(snapshot.FilePath, obj.FilePath); Assert.Equal(snapshot.Configuration.ConfigurationName, obj.Configuration.ConfigurationName); Assert.Collection( - snapshot.Configuration.Extensions.OrderBy(e => e.ExtensionName), + snapshot.Configuration.Extensions.OrderBy(e => e.ExtensionName), e => Assert.Equal("Test-Extension1", e.ExtensionName), e => Assert.Equal("Test-Extension2", e.ExtensionName)); Assert.Equal(snapshot.Configuration.LanguageVersion, obj.Configuration.LanguageVersion); - Assert.Equal(snapshot.WorkspaceProjectId.Id, obj.WorkspaceProjectId.Id); } [Fact] public void ProjectSnapshotHandleJsonConverter_SerializationWithNulls_CanKindaRoundTrip() { // Arrange - var snapshot = new ProjectSnapshotHandle("Test.csproj", null, null); + var snapshot = new ProjectSnapshotHandle("Test.csproj", null); // Act var json = JsonConvert.SerializeObject(snapshot, Converters); @@ -67,7 +63,6 @@ public void ProjectSnapshotHandleJsonConverter_SerializationWithNulls_CanKindaRo // Assert Assert.Equal(snapshot.FilePath, obj.FilePath); Assert.Null(obj.Configuration); - Assert.Null(obj.WorkspaceProjectId); } } } diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/VsSolutionUpdatesProjectSnapshotChangeTriggerTest.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/VsSolutionUpdatesProjectSnapshotChangeTriggerTest.cs index d180992d835..a39cc26b80b 100644 --- a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/VsSolutionUpdatesProjectSnapshotChangeTriggerTest.cs +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/VsSolutionUpdatesProjectSnapshotChangeTriggerTest.cs @@ -2,10 +2,12 @@ // 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; using Microsoft.CodeAnalysis.Razor; using Microsoft.CodeAnalysis.Razor.ProjectSystem; using Microsoft.VisualStudio.Editor.Razor; +using Microsoft.VisualStudio.LanguageServices.Razor.Test; using Microsoft.VisualStudio.Shell.Interop; using Moq; using Xunit; @@ -28,14 +30,6 @@ public VsSolutionUpdatesProjectSnapshotChangeTriggerTest() "SomeProject", LanguageNames.CSharp, filePath: SomeProject.FilePath)); - - SomeOtherWorkspaceProject = w.AddProject(ProjectInfo.Create( - ProjectId.CreateNewId(), - VersionStamp.Create(), - "SomeOtherProject", - "SomeOtherProject", - LanguageNames.CSharp, - filePath: SomeOtherProject.FilePath)); }); } @@ -45,8 +39,6 @@ public VsSolutionUpdatesProjectSnapshotChangeTriggerTest() private Project SomeWorkspaceProject { get; set; } - private Project SomeOtherWorkspaceProject { get; set; } - private Workspace Workspace { get; } [Fact] @@ -63,7 +55,10 @@ public void Initialize_AttachesEventSink() var services = new Mock(); services.Setup(s => s.GetService(It.Is(f => f == typeof(SVsSolutionBuildManager)))).Returns(buildManager.Object); - var trigger = new VsSolutionUpdatesProjectSnapshotChangeTrigger(services.Object, Mock.Of()); + var trigger = new VsSolutionUpdatesProjectSnapshotChangeTrigger( + services.Object, + Mock.Of(), + Mock.Of()); // Act trigger.Initialize(Mock.Of()); @@ -73,7 +68,7 @@ public void Initialize_AttachesEventSink() } [Fact] - public void UpdateProjectCfg_Done_KnownProject_Invokes_WorkspaceProjectChanged() + public void UpdateProjectCfg_Done_KnownProject_EnqueuesProjectStateUpdate() { // Arrange var expectedProjectPath = SomeProject.FilePath; @@ -92,40 +87,33 @@ public void UpdateProjectCfg_Done_KnownProject_Invokes_WorkspaceProjectChanged() var projectSnapshots = new[] { - new DefaultProjectSnapshot(ProjectState.Create(Workspace.Services, SomeProject, SomeWorkspaceProject)), - new DefaultProjectSnapshot(ProjectState.Create(Workspace.Services, SomeOtherProject, SomeOtherWorkspaceProject)), + new DefaultProjectSnapshot(ProjectState.Create(Workspace.Services, SomeProject)), + new DefaultProjectSnapshot(ProjectState.Create(Workspace.Services, SomeOtherProject)), }; - var called = false; var projectManager = new Mock(); projectManager.SetupGet(p => p.Workspace).Returns(Workspace); projectManager .Setup(p => p.GetLoadedProject(expectedProjectPath)) .Returns(projectSnapshots[0]); - projectManager - .Setup(p => p.WorkspaceProjectChanged(It.IsAny())) - .Callback(c => - { - called = true; - Assert.Equal(expectedProjectPath, c.FilePath); - }); - - var trigger = new VsSolutionUpdatesProjectSnapshotChangeTrigger(services.Object, projectService.Object); + var workspaceStateGenerator = new TestProjectWorkspaceStateGenerator(); + + var trigger = new VsSolutionUpdatesProjectSnapshotChangeTrigger(services.Object, projectService.Object, workspaceStateGenerator); trigger.Initialize(projectManager.Object); // Act trigger.UpdateProjectCfg_Done(Mock.Of(), Mock.Of(), Mock.Of(), 0, 0, 0); // Assert - Assert.True(called); + var update = Assert.Single(workspaceStateGenerator.UpdateQueue); + Assert.Equal(update.workspaceProject.Id, SomeWorkspaceProject.Id); + Assert.Same(update.projectSnapshot, projectSnapshots[0]); } [Fact] - public void UpdateProjectCfg_Done_WithoutWorkspaceProject_DoesNotInvoke_WorkspaceProjectChanged() + public void UpdateProjectCfg_Done_WithoutWorkspaceProject_DoesNotEnqueueUpdate() { // Arrange - var expectedProjectPath = SomeProject.FilePath; - uint cookie; var buildManager = new Mock(MockBehavior.Strict); buildManager @@ -134,37 +122,34 @@ public void UpdateProjectCfg_Done_WithoutWorkspaceProject_DoesNotInvoke_Workspac var services = new Mock(); services.Setup(s => s.GetService(It.Is(f => f == typeof(SVsSolutionBuildManager)))).Returns(buildManager.Object); + var projectSnapshot = new DefaultProjectSnapshot( + ProjectState.Create( + Workspace.Services, + new HostProject("/Some/Unknown/Path.csproj", RazorConfiguration.Default))); + var expectedProjectPath = projectSnapshot.FilePath; var projectService = new Mock(); projectService.Setup(p => p.GetProjectPath(It.IsAny())).Returns(expectedProjectPath); - var projectSnapshots = new[] - { - new DefaultProjectSnapshot(ProjectState.Create(Workspace.Services, SomeProject, null)), - new DefaultProjectSnapshot(ProjectState.Create(Workspace.Services, SomeOtherProject, SomeOtherWorkspaceProject)), - }; - var projectManager = new Mock(); projectManager.SetupGet(p => p.Workspace).Returns(Workspace); projectManager .Setup(p => p.GetLoadedProject(expectedProjectPath)) - .Returns(projectSnapshots[0]); - projectManager - .Setup(p => p.WorkspaceProjectChanged(It.IsAny())) - .Callback(c => - { - throw new InvalidOperationException("This should not be called."); - }); + .Returns(projectSnapshot); + var workspaceStateGenerator = new TestProjectWorkspaceStateGenerator(); - var trigger = new VsSolutionUpdatesProjectSnapshotChangeTrigger(services.Object, projectService.Object); + var trigger = new VsSolutionUpdatesProjectSnapshotChangeTrigger(services.Object, projectService.Object, workspaceStateGenerator); trigger.Initialize(projectManager.Object); - // Act & Assert - Does not throw + // Act trigger.UpdateProjectCfg_Done(Mock.Of(), Mock.Of(), Mock.Of(), 0, 0, 0); + + // Assert + Assert.Empty(workspaceStateGenerator.UpdateQueue); } [Fact] - public void UpdateProjectCfg_Done_UnknownProject_DoesNotInvoke_WorkspaceProjectChanged() + public void UpdateProjectCfg_Done_UnknownProject_DoesNotEnqueueUpdate() { // Arrange var expectedProjectPath = "Path/To/Project"; @@ -181,29 +166,21 @@ public void UpdateProjectCfg_Done_UnknownProject_DoesNotInvoke_WorkspaceProjectC var projectService = new Mock(); projectService.Setup(p => p.GetProjectPath(It.IsAny())).Returns(expectedProjectPath); - var projectSnapshots = new[] - { - new DefaultProjectSnapshot(ProjectState.Create(Workspace.Services, SomeProject, SomeWorkspaceProject)), - new DefaultProjectSnapshot(ProjectState.Create(Workspace.Services, SomeOtherProject, SomeOtherWorkspaceProject)), - }; - var projectManager = new Mock(); projectManager.SetupGet(p => p.Workspace).Returns(Workspace); projectManager .Setup(p => p.GetLoadedProject(expectedProjectPath)) .Returns((ProjectSnapshot)null); - projectManager - .Setup(p => p.WorkspaceProjectChanged(It.IsAny())) - .Callback(c => - { - throw new InvalidOperationException("This should not be called."); - }); + var workspaceStateGenerator = new TestProjectWorkspaceStateGenerator(); - var trigger = new VsSolutionUpdatesProjectSnapshotChangeTrigger(services.Object, projectService.Object); + var trigger = new VsSolutionUpdatesProjectSnapshotChangeTrigger(services.Object, projectService.Object, workspaceStateGenerator); trigger.Initialize(projectManager.Object); - // Act & Assert - Does not throw + // Act trigger.UpdateProjectCfg_Done(Mock.Of(), Mock.Of(), Mock.Of(), 0, 0, 0); + + // Assert + Assert.Empty(workspaceStateGenerator.UpdateQueue); } } } diff --git a/src/Razor/test/Microsoft.VisualStudio.Mac.LanguageServices.Razor.Test/ProjectBuildChangeTriggerTest.cs b/src/Razor/test/Microsoft.VisualStudio.Mac.LanguageServices.Razor.Test/ProjectBuildChangeTriggerTest.cs index b0ca2cdfe7a..69e53d239b7 100644 --- a/src/Razor/test/Microsoft.VisualStudio.Mac.LanguageServices.Razor.Test/ProjectBuildChangeTriggerTest.cs +++ b/src/Razor/test/Microsoft.VisualStudio.Mac.LanguageServices.Razor.Test/ProjectBuildChangeTriggerTest.cs @@ -6,6 +6,7 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Razor.ProjectSystem; using Microsoft.VisualStudio.Editor.Razor; +using Microsoft.VisualStudio.Editor.Razor.Test; using MonoDevelop.Projects; using Moq; using Xunit; @@ -30,14 +31,6 @@ public ProjectBuildChangeTriggerTest() "SomeProject", LanguageNames.CSharp, filePath: SomeProject.FilePath)); - - SomeOtherWorkspaceProject = w.AddProject(ProjectInfo.Create( - ProjectId.CreateNewId(), - VersionStamp.Create(), - "SomeOtherProject", - "SomeOtherProject", - LanguageNames.CSharp, - filePath: SomeOtherProject.FilePath)); }); } @@ -47,97 +40,83 @@ public ProjectBuildChangeTriggerTest() private Project SomeWorkspaceProject { get; set; } - private Project SomeOtherWorkspaceProject { get; set; } - private Workspace Workspace { get; } [ForegroundFact] - public void ProjectOperations_EndBuild_Invokes_WorkspaceProjectChanged() + public void ProjectOperations_EndBuild_EnqueuesProjectStateUpdate() { // Arrange var expectedProjectPath = SomeProject.FilePath; var projectService = CreateProjectService(expectedProjectPath); var args = new BuildEventArgs(monitor: null, success: true); - - var projectSnapshots = new[] - { - new DefaultProjectSnapshot(ProjectState.Create(Workspace.Services, SomeProject, SomeWorkspaceProject)), - new DefaultProjectSnapshot(ProjectState.Create(Workspace.Services, SomeOtherProject, SomeOtherWorkspaceProject)), - }; + var projectSnapshot = new DefaultProjectSnapshot(ProjectState.Create(Workspace.Services, SomeProject)); var projectManager = new Mock(MockBehavior.Strict); projectManager.SetupGet(p => p.Workspace).Returns(Workspace); projectManager .Setup(p => p.GetLoadedProject(SomeProject.FilePath)) - .Returns(projectSnapshots[0]); - projectManager - .Setup(p => p.WorkspaceProjectChanged(It.IsAny())) - .Callback(c => Assert.Equal(expectedProjectPath, c.FilePath)); - - var trigger = new ProjectBuildChangeTrigger(Dispatcher, projectService, projectManager.Object); + .Returns(projectSnapshot); + var workspaceStateGenerator = new TestProjectWorkspaceStateGenerator(); + var trigger = new ProjectBuildChangeTrigger(Dispatcher, projectService, workspaceStateGenerator, projectManager.Object); // Act trigger.ProjectOperations_EndBuild(null, args); // Assert - projectManager.VerifyAll(); + var update = Assert.Single(workspaceStateGenerator.UpdateQueue); + Assert.Equal(SomeWorkspaceProject, update.workspaceProject); } [ForegroundFact] public void ProjectOperations_EndBuild_ProjectWithoutWorkspaceProject_Noops() { // Arrange - var projectService = CreateProjectService(SomeProject.FilePath); + var expectedPath = "Path/To/Project.csproj"; + var projectService = CreateProjectService(expectedPath); var args = new BuildEventArgs(monitor: null, success: true); - var projectSnapshots = new[] - { - new DefaultProjectSnapshot(ProjectState.Create(Workspace.Services, SomeProject, null)), - new DefaultProjectSnapshot(ProjectState.Create(Workspace.Services, SomeOtherProject, SomeOtherWorkspaceProject)), - }; + var projectSnapshot = new DefaultProjectSnapshot( + ProjectState.Create( + Workspace.Services, + new HostProject(expectedPath, RazorConfiguration.Default))); var projectManager = new Mock(); projectManager.SetupGet(p => p.Workspace).Returns(Workspace); projectManager - .Setup(p => p.GetLoadedProject(SomeProject.FilePath)) - .Returns(projectSnapshots[0]); - projectManager - .Setup(p => p.WorkspaceProjectChanged(It.IsAny())) - .Throws(); - - var trigger = new ProjectBuildChangeTrigger(Dispatcher, projectService, projectManager.Object); + .Setup(p => p.GetLoadedProject(expectedPath)) + .Returns(projectSnapshot); + var workspaceStateGenerator = new TestProjectWorkspaceStateGenerator(); + var trigger = new ProjectBuildChangeTrigger(Dispatcher, projectService, workspaceStateGenerator, projectManager.Object); - // Act & Assert + // Act trigger.ProjectOperations_EndBuild(null, args); + + // Assert + Assert.Empty(workspaceStateGenerator.UpdateQueue); } [ForegroundFact] public void ProjectOperations_EndBuild_UntrackedProject_Noops() { // Arrange - var projectService = CreateProjectService("Path/To/Project"); + var projectService = CreateProjectService(SomeProject.FilePath); var args = new BuildEventArgs(monitor: null, success: true); - var projectSnapshots = new[] - { - new DefaultProjectSnapshot(ProjectState.Create(Workspace.Services, SomeProject, null)), - new DefaultProjectSnapshot(ProjectState.Create(Workspace.Services, SomeOtherProject, SomeOtherWorkspaceProject)), - }; var projectManager = new Mock(); projectManager.SetupGet(p => p.Workspace).Returns(Workspace); projectManager .Setup(p => p.GetLoadedProject(SomeProject.FilePath)) - .Returns(projectSnapshots[0]); - projectManager - .Setup(p => p.WorkspaceProjectChanged(It.IsAny())) - .Throws(); - - var trigger = new ProjectBuildChangeTrigger(Dispatcher, projectService, projectManager.Object); + .Returns(null); + var workspaceStateGenerator = new TestProjectWorkspaceStateGenerator(); + var trigger = new ProjectBuildChangeTrigger(Dispatcher, projectService, workspaceStateGenerator, projectManager.Object); - // Act & Assert + // Act trigger.ProjectOperations_EndBuild(null, args); + + // Assert + Assert.Empty(workspaceStateGenerator.UpdateQueue); } [ForegroundFact] @@ -149,10 +128,14 @@ public void ProjectOperations_EndBuild_BuildFailed_Noops() projectService.Setup(p => p.IsSupportedProject(null)).Throws(); var projectManager = new Mock(); projectManager.SetupGet(p => p.Workspace).Throws(); - var trigger = new ProjectBuildChangeTrigger(Dispatcher, projectService.Object, projectManager.Object); + var workspaceStateGenerator = new TestProjectWorkspaceStateGenerator(); + var trigger = new ProjectBuildChangeTrigger(Dispatcher, projectService.Object, workspaceStateGenerator, projectManager.Object); - // Act & Assert + // Act trigger.ProjectOperations_EndBuild(null, args); + + // Assert + Assert.Empty(workspaceStateGenerator.UpdateQueue); } [ForegroundFact] @@ -164,10 +147,14 @@ public void ProjectOperations_EndBuild_UnsupportedProject_Noops() projectService.Setup(p => p.IsSupportedProject(null)).Returns(false); var projectManager = new Mock(); projectManager.SetupGet(p => p.Workspace).Throws(); - var trigger = new ProjectBuildChangeTrigger(Dispatcher, projectService.Object, projectManager.Object); + var workspaceStateGenerator = new TestProjectWorkspaceStateGenerator(); + var trigger = new ProjectBuildChangeTrigger(Dispatcher, projectService.Object, workspaceStateGenerator, projectManager.Object); - // Act & Assert + // Act trigger.ProjectOperations_EndBuild(null, args); + + // Assert + Assert.Empty(workspaceStateGenerator.UpdateQueue); } private static TextBufferProjectService CreateProjectService(string projectPath)