Skip to content
Merged
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT license. See License.txt in the project root for license information.

#if !NET
using System;
#endif

using System.Diagnostics;
using Microsoft.AspNetCore.Razor.Utilities;
using Microsoft.CodeAnalysis;

namespace Microsoft.AspNetCore.Razor.ProjectSystem;

internal static class Extensions
{
/// <summary>
/// Returns <see langword="true"/> if this <see cref="ProjectKey"/> matches the given <see cref="Project"/>.
/// </summary>
public static bool Matches(this ProjectKey projectKey, Project project)
{
// In order to perform this check, we are relying on the fact that Id will always end with a '/',
// because it is guaranteed to be normalized. However, CompilationOutputInfo.AssemblyPath will
// contain the assembly file name, which AreDirectoryPathsEquivalent will shave off before comparing.
// So, AreDirectoryPathsEquivalent will return true when Id is "C:/my/project/path/"
// and the assembly path is "C:\my\project\path\assembly.dll"

Debug.Assert(projectKey.Id.EndsWith('/'), $"This method can't be called if {nameof(projectKey.Id)} is not a normalized directory path.");

return FilePathNormalizer.AreDirectoryPathsEquivalent(projectKey.Id, project.CompilationOutputInfo.AssemblyPath);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT license. See License.txt in the project root for license information.

using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Microsoft.AspNetCore.Razor;
using Microsoft.AspNetCore.Razor.ProjectSystem;

namespace Microsoft.CodeAnalysis;

internal static class SolutionExtensions
{
public static bool TryGetProject(this Solution solution, ProjectId projectId, [NotNullWhen(true)] out Project? result)
{
result = solution.GetProject(projectId);
return result is not null;
}

public static Project GetRequiredProject(this Solution solution, ProjectId projectId)
{
return solution.GetProject(projectId)
?? ThrowHelper.ThrowInvalidOperationException<Project>($"The project {projectId} did not exist in {solution}.");
}

public static bool TryGetDocument(this Solution solution, DocumentId documentId, [NotNullWhen(true)] out Document? result)
{
result = solution.GetDocument(documentId);
return result is not null;
}

public static Document GetRequiredDocument(this Solution solution, DocumentId documentId)
{
return solution.GetDocument(documentId)
?? ThrowHelper.ThrowInvalidOperationException<Document>($"The document {documentId} did not exist in {solution.FilePath ?? "solution"}.");
}

public static Project? GetProject(this Solution solution, ProjectKey projectKey)
{
return solution.Projects.FirstOrDefault(project => projectKey.Matches(project));
}

public static bool TryGetProject(this Solution solution, ProjectKey projectKey, [NotNullWhen(true)] out Project? result)
{
result = solution.GetProject(projectKey);
return result is not null;
}

public static Project GetRequiredProject(this Solution solution, ProjectKey projectKey)
{
return solution.GetProject(projectKey)
?? ThrowHelper.ThrowInvalidOperationException<Project>($"The project {projectKey} did not exist in {solution}.");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Microsoft.AspNetCore.Razor;
using Microsoft.CodeAnalysis.Razor;

namespace Microsoft.CodeAnalysis;
Expand All @@ -30,16 +29,4 @@ public static bool TryGetRazorDocument(this Solution solution, Uri razorDocument
razorDocument = document;
return true;
}

public static Project GetRequiredProject(this Solution solution, ProjectId projectId)
{
return solution.GetProject(projectId)
?? ThrowHelper.ThrowInvalidOperationException<Project>($"The projectId {projectId} did not exist in {solution}.");
}

public static Document GetRequiredDocument(this Solution solution, DocumentId documentId)
{
return solution.GetDocument(documentId)
?? ThrowHelper.ThrowInvalidOperationException<Document>($"The document {documentId} did not exist in {solution.FilePath ?? "solution"}.");
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,6 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT license. See License.txt in the project root for license information.

#if !NET
using System;
#endif

using System.Diagnostics;
using Microsoft.AspNetCore.Razor.ProjectSystem;
using Microsoft.AspNetCore.Razor.Serialization;
using Microsoft.AspNetCore.Razor.Utilities;
Expand All @@ -22,20 +17,4 @@ public static ProjectKey ToProjectKey(this Project project)
var intermediateOutputPath = FilePathNormalizer.GetNormalizedDirectoryName(project.CompilationOutputInfo.AssemblyPath);
return new(intermediateOutputPath);
}

/// <summary>
/// Returns <see langword="true"/> if this <see cref="ProjectKey"/> matches the given <see cref="Project"/>.
/// </summary>
public static bool Matches(this ProjectKey projectKey, Project project)
{
// In order to perform this check, we are relying on the fact that Id will always end with a '/',
// because it is guaranteed to be normalized. However, CompilationOutputInfo.AssemblyPath will
// contain the assembly file name, which AreDirectoryPathsEquivalent will shave off before comparing.
// So, AreDirectoryPathsEquivalent will return true when Id is "C:/my/project/path/"
// and the assembly path is "C:\my\project\path\assembly.dll"

Debug.Assert(projectKey.Id.EndsWith('/'), $"This method can't be called if {nameof(projectKey.Id)} is not a normalized directory path.");

return FilePathNormalizer.AreDirectoryPathsEquivalent(projectKey.Id, project.CompilationOutputInfo.AssemblyPath);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT license. See License.txt in the project root for license information.

using Microsoft.AspNetCore.Razor.ProjectSystem;
using Microsoft.CodeAnalysis;

namespace Microsoft.VisualStudio.Razor.Discovery;

internal interface IProjectStateUpdater
{
void EnqueueUpdate(ProjectKey key, ProjectId? id);

void CancelUpdates();
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
using Microsoft.CodeAnalysis.Razor.Remote;
using Microsoft.CodeAnalysis.Razor.Workspaces;

namespace Microsoft.VisualStudio.Razor.Remote;
namespace Microsoft.VisualStudio.Razor.Discovery;

/// <summary>
/// Retrieves <see cref="TagHelperDescriptor">tag helpers</see> for a given <see cref="Project"/>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT license. See License.txt in the project root for license information.

using System.Threading;
using Microsoft.VisualStudio.Shell.Interop;
using Microsoft.VisualStudio.Threading;
using Task = System.Threading.Tasks.Task;

namespace Microsoft.VisualStudio.Razor.Discovery;

internal sealed partial class ProjectBuildDetector
{
internal TestAccessor GetTestAccessor() => new(this);

internal readonly struct TestAccessor(ProjectBuildDetector instance)
{
public JoinableTask InitializeTask => instance._initializeTask;
public Task? OnProjectBuiltTask => instance._projectBuiltTask;

public Task OnProjectBuiltAsync(IVsHierarchy projectHierarchy, CancellationToken cancellationToken)
=> instance.OnProjectBuiltAsync(projectHierarchy, cancellationToken);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@

using System;
using System.ComponentModel.Composition;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
using Microsoft.CodeAnalysis.Razor.Workspaces;
using Microsoft.VisualStudio.Razor.Extensions;
Expand All @@ -14,14 +14,14 @@
using Microsoft.VisualStudio.Threading;
using Task = System.Threading.Tasks.Task;

namespace Microsoft.VisualStudio.Razor;
namespace Microsoft.VisualStudio.Razor.Discovery;

[Export(typeof(IRazorStartupService))]
internal class VsSolutionUpdatesProjectSnapshotChangeTrigger : IRazorStartupService, IVsUpdateSolutionEvents2, IDisposable
internal sealed partial class ProjectBuildDetector : IRazorStartupService, IVsUpdateSolutionEvents2, IDisposable
{
private readonly IServiceProvider _serviceProvider;
private readonly ProjectSnapshotManager _projectManager;
private readonly IProjectWorkspaceStateGenerator _workspaceStateGenerator;
private readonly IProjectStateUpdater _projectStateUpdater;
private readonly IWorkspaceProvider _workspaceProvider;
private readonly JoinableTaskFactory _jtf;
private readonly CancellationTokenSource _disposeTokenSource;
Expand All @@ -33,16 +33,16 @@ internal class VsSolutionUpdatesProjectSnapshotChangeTrigger : IRazorStartupServ
private Task? _projectBuiltTask;

[ImportingConstructor]
public VsSolutionUpdatesProjectSnapshotChangeTrigger(
public ProjectBuildDetector(
[Import(typeof(SVsServiceProvider))] IServiceProvider serviceProvider,
ProjectSnapshotManager projectManager,
IProjectWorkspaceStateGenerator workspaceStateGenerator,
IProjectStateUpdater projectStateUpdater,
IWorkspaceProvider workspaceProvider,
JoinableTaskContext joinableTaskContext)
{
_serviceProvider = serviceProvider;
_projectManager = projectManager;
_workspaceStateGenerator = workspaceStateGenerator;
_projectStateUpdater = projectStateUpdater;
_workspaceProvider = workspaceProvider;
_jtf = joinableTaskContext.Factory;

Expand Down Expand Up @@ -110,7 +110,7 @@ private void ProjectManager_Changed(object sender, ProjectChangeEventArgs args)
if (args.IsSolutionClosing)
{
// If the solution is closing, cancel all existing updates.
_workspaceStateGenerator.CancelUpdates();
_projectStateUpdater.CancelUpdates();
}
}

Expand All @@ -123,30 +123,22 @@ private async Task OnProjectBuiltAsync(IVsHierarchy projectHierarchy, Cancellati
}

var projectKeys = _projectManager.GetProjectKeysWithFilePath(projectFilePath);
if (projectKeys.IsEmpty)
{
return;
}

var workspace = _workspaceProvider.GetWorkspace();
var solution = workspace.CurrentSolution;

foreach (var projectKey in projectKeys)
{
if (_projectManager.TryGetProject(projectKey, out var project))
if (solution.TryGetProject(projectKey, out var workspaceProject))
{
var workspace = _workspaceProvider.GetWorkspace();
var workspaceProject = workspace.CurrentSolution.Projects.FirstOrDefault(wp => wp.ToProjectKey() == project.Key);
if (workspaceProject is not null)
{
// Trigger a tag helper update by forcing the project manager to see the workspace Project
// from the current solution.
_workspaceStateGenerator.EnqueueUpdate(workspaceProject, project);
}
// Trigger a tag helper update by forcing the project manager to see the workspace Project
// from the current solution.
_projectStateUpdater.EnqueueUpdate(projectKey, workspaceProject.Id);
}
}
}

internal TestAccessor GetTestAccessor() => new(this);

internal sealed class TestAccessor(VsSolutionUpdatesProjectSnapshotChangeTrigger instance)
{
public JoinableTask InitializeTask => instance._initializeTask;
public Task? OnProjectBuiltTask => instance._projectBuiltTask;

public Task OnProjectBuiltAsync(IVsHierarchy projectHierarchy, CancellationToken cancellationToken)
=> instance.OnProjectBuiltAsync(projectHierarchy, cancellationToken);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT license. See License.txt in the project root for license information.

using System.Collections.Generic;

namespace Microsoft.VisualStudio.Razor.Discovery;

internal partial class ProjectStateChangeDetector
{
private sealed class Comparer : IEqualityComparer<Work>
{
public static readonly Comparer Instance = new();

private Comparer()
{
}

public bool Equals(Work x, Work y)
=> x.Key == y.Key;

public int GetHashCode(Work obj)
=> obj.Key.GetHashCode();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT license. See License.txt in the project root for license information.

using System;
using System.Collections.Immutable;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;

namespace Microsoft.VisualStudio.Razor.Discovery;

internal partial class ProjectStateChangeDetector
{
internal TestAccessor GetTestAccessor() => new(this);

internal sealed class TestAccessor(ProjectStateChangeDetector instance)
{
public void CancelExistingWork()
{
instance._workQueue.CancelExistingWork();
}

public async Task WaitUntilCurrentBatchCompletesAsync()
{
await instance._workQueue.WaitUntilCurrentBatchCompletesAsync();
}

public Task ListenForWorkspaceChangesAsync(params WorkspaceChangeKind[] kinds)
{
if (instance._workspaceChangedListener is not null)
{
throw new InvalidOperationException($"There's already a {nameof(WorkspaceChangedListener)} installed.");
}

var listener = new WorkspaceChangedListener(kinds.ToImmutableArray());
instance._workspaceChangedListener = listener;

return listener.Task;
}

public void WorkspaceChanged(WorkspaceChangeEventArgs e)
{
instance.Workspace_WorkspaceChanged(instance, e);
}
}
}
Loading
Loading