463 lines
19 KiB
C#
463 lines
19 KiB
C#
// Copyright (c) .NET Foundation. All rights reserved.
|
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.Linq;
|
|
|
|
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 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.
|
|
internal class DefaultProjectSnapshotManager : ProjectSnapshotManagerBase
|
|
{
|
|
public override event EventHandler<ProjectChangeEventArgs> Changed;
|
|
|
|
private readonly ErrorReporter _errorReporter;
|
|
private readonly ForegroundDispatcher _foregroundDispatcher;
|
|
private readonly ProjectSnapshotChangeTrigger[] _triggers;
|
|
private readonly ProjectSnapshotWorkerQueue _workerQueue;
|
|
private readonly ProjectSnapshotWorker _worker;
|
|
|
|
private readonly Dictionary<string, DefaultProjectSnapshot> _projects;
|
|
|
|
public DefaultProjectSnapshotManager(
|
|
ForegroundDispatcher foregroundDispatcher,
|
|
ErrorReporter errorReporter,
|
|
ProjectSnapshotWorker worker,
|
|
IEnumerable<ProjectSnapshotChangeTrigger> triggers,
|
|
Workspace workspace)
|
|
{
|
|
if (foregroundDispatcher == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(foregroundDispatcher));
|
|
}
|
|
|
|
if (errorReporter == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(errorReporter));
|
|
}
|
|
|
|
if (worker == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(worker));
|
|
}
|
|
|
|
if (triggers == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(triggers));
|
|
}
|
|
|
|
if (workspace == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(workspace));
|
|
}
|
|
|
|
_foregroundDispatcher = foregroundDispatcher;
|
|
_errorReporter = errorReporter;
|
|
_worker = worker;
|
|
_triggers = triggers.ToArray();
|
|
Workspace = workspace;
|
|
|
|
_projects = new Dictionary<string, DefaultProjectSnapshot>(FilePathComparer.Instance);
|
|
|
|
_workerQueue = new ProjectSnapshotWorkerQueue(_foregroundDispatcher, this, worker);
|
|
|
|
for (var i = 0; i < _triggers.Length; i++)
|
|
{
|
|
_triggers[i].Initialize(this);
|
|
}
|
|
}
|
|
|
|
public override IReadOnlyList<ProjectSnapshot> Projects
|
|
{
|
|
get
|
|
{
|
|
_foregroundDispatcher.AssertForegroundThread();
|
|
return _projects.Values.ToArray();
|
|
}
|
|
}
|
|
|
|
public override Workspace Workspace { get; }
|
|
|
|
public override void ProjectUpdated(ProjectSnapshotUpdateContext update)
|
|
{
|
|
if (update == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(update));
|
|
}
|
|
|
|
_foregroundDispatcher.AssertForegroundThread();
|
|
|
|
if (_projects.TryGetValue(update.WorkspaceProject.FilePath, out var original))
|
|
{
|
|
if (!original.IsInitialized)
|
|
{
|
|
// If the project has been uninitialized, just ignore the update.
|
|
return;
|
|
}
|
|
|
|
// This is an update to the project's computed values, so everything should be overwritten
|
|
var snapshot = original.WithComputedUpdate(update);
|
|
_projects[update.WorkspaceProject.FilePath] = snapshot;
|
|
|
|
if (snapshot.IsDirty)
|
|
{
|
|
// It's possible that the snapshot can still be dirty if we got a project update while computing state in
|
|
// the background. We need to trigger the background work to asynchronously compute the effect of the updates.
|
|
NotifyBackgroundWorker(snapshot.CreateUpdateContext());
|
|
}
|
|
|
|
if (!object.Equals(snapshot.ComputedVersion, original.ComputedVersion))
|
|
{
|
|
NotifyListeners(new ProjectChangeEventArgs(snapshot, ProjectChangeKind.TagHelpersChanged));
|
|
}
|
|
}
|
|
}
|
|
|
|
public override void HostProjectAdded(HostProject hostProject)
|
|
{
|
|
if (hostProject == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(hostProject));
|
|
}
|
|
|
|
_foregroundDispatcher.AssertForegroundThread();
|
|
|
|
// We don't expect to see a HostProject initialized multiple times for the same path. Just ignore it.
|
|
if (_projects.ContainsKey(hostProject.FilePath))
|
|
{
|
|
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 snapshot = new DefaultProjectSnapshot(hostProject, workspaceProject);
|
|
_projects[hostProject.FilePath] = snapshot;
|
|
|
|
if (snapshot.IsInitialized && snapshot.IsDirty)
|
|
{
|
|
// Start computing background state if the project is fully initialized.
|
|
NotifyBackgroundWorker(snapshot.CreateUpdateContext());
|
|
}
|
|
|
|
// We need to notify listeners about every project add.
|
|
NotifyListeners(new ProjectChangeEventArgs(snapshot, ProjectChangeKind.Added));
|
|
}
|
|
|
|
public override void HostProjectChanged(HostProject hostProject)
|
|
{
|
|
if (hostProject == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(hostProject));
|
|
}
|
|
|
|
_foregroundDispatcher.AssertForegroundThread();
|
|
|
|
if (_projects.TryGetValue(hostProject.FilePath, out var original))
|
|
{
|
|
// Doing an update to the project should keep computed values, but mark the project as dirty if the
|
|
// underlying project is newer.
|
|
var snapshot = original.WithHostProject(hostProject);
|
|
_projects[hostProject.FilePath] = snapshot;
|
|
|
|
if (snapshot.IsInitialized && snapshot.IsDirty)
|
|
{
|
|
// Start computing background state if the project is fully initialized.
|
|
NotifyBackgroundWorker(snapshot.CreateUpdateContext());
|
|
}
|
|
|
|
// Notify listeners right away because if the HostProject changes then it's likely that the Razor
|
|
// configuration changed.
|
|
NotifyListeners(new ProjectChangeEventArgs(snapshot, ProjectChangeKind.Changed));
|
|
}
|
|
}
|
|
|
|
public override void HostProjectRemoved(HostProject hostProject)
|
|
{
|
|
if (hostProject == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(hostProject));
|
|
}
|
|
|
|
_foregroundDispatcher.AssertForegroundThread();
|
|
|
|
if (_projects.TryGetValue(hostProject.FilePath, out var snapshot))
|
|
{
|
|
_projects.Remove(hostProject.FilePath);
|
|
|
|
// We need to notify listeners about every project removal.
|
|
NotifyListeners(new ProjectChangeEventArgs(snapshot, ProjectChangeKind.Removed));
|
|
}
|
|
}
|
|
|
|
public override void HostProjectBuildComplete(HostProject hostProject)
|
|
{
|
|
if (hostProject == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(hostProject));
|
|
}
|
|
|
|
_foregroundDispatcher.AssertForegroundThread();
|
|
|
|
if (_projects.TryGetValue(hostProject.FilePath, out var original))
|
|
{
|
|
var workspaceProject = GetWorkspaceProject(hostProject.FilePath);
|
|
if (workspaceProject == null)
|
|
{
|
|
// Host project was built prior to a workspace project being associated. We have nothing to do without
|
|
// a workspace project so we short circuit.
|
|
return;
|
|
}
|
|
|
|
// Doing an update to the project should keep computed values, but mark the project as dirty if the
|
|
// underlying project is newer.
|
|
var snapshot = original.WithWorkspaceProject(workspaceProject);
|
|
|
|
_projects[hostProject.FilePath] = snapshot;
|
|
|
|
// Notify the background worker so it can trigger tag helper discovery.
|
|
NotifyBackgroundWorker(snapshot.CreateUpdateContext());
|
|
}
|
|
}
|
|
|
|
public override void WorkspaceProjectAdded(Project workspaceProject)
|
|
{
|
|
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 original))
|
|
{
|
|
// If this is a multi-targeting project then we are only interested in a single workspace project. If we already
|
|
// found one in the past just ignore this one.
|
|
if (original.WorkspaceProject == null)
|
|
{
|
|
var snapshot = original.WithWorkspaceProject(workspaceProject);
|
|
_projects[workspaceProject.FilePath] = snapshot;
|
|
|
|
if (snapshot.IsInitialized && snapshot.IsDirty)
|
|
{
|
|
// We don't need to notify listeners yet because we don't have any **new** computed state.
|
|
//
|
|
// However we do need to trigger the background work to asynchronously compute the effect of the updates.
|
|
NotifyBackgroundWorker(snapshot.CreateUpdateContext());
|
|
}
|
|
|
|
// Notify listeners right away since WorkspaceProject was just added, the project is now initialized.
|
|
NotifyListeners(new ProjectChangeEventArgs(snapshot, ProjectChangeKind.Changed));
|
|
}
|
|
}
|
|
}
|
|
|
|
public override void WorkspaceProjectChanged(Project workspaceProject)
|
|
{
|
|
if (workspaceProject == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(workspaceProject));
|
|
}
|
|
|
|
_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 original) &&
|
|
(original.WorkspaceProject == null ||
|
|
original.WorkspaceProject.Id == workspaceProject.Id))
|
|
{
|
|
// Doing an update to the project should keep computed values, but mark the project as dirty if the
|
|
// underlying project is newer.
|
|
var snapshot = original.WithWorkspaceProject(workspaceProject);
|
|
_projects[workspaceProject.FilePath] = snapshot;
|
|
|
|
if (snapshot.IsInitialized && snapshot.IsDirty)
|
|
{
|
|
// We don't need to notify listeners yet because we don't have any **new** computed state. However we do
|
|
// need to trigger the background work to asynchronously compute the effect of the updates.
|
|
NotifyBackgroundWorker(snapshot.CreateUpdateContext());
|
|
}
|
|
|
|
if (snapshot.HaveTagHelpersChanged(original))
|
|
{
|
|
NotifyListeners(new ProjectChangeEventArgs(snapshot, ProjectChangeKind.TagHelpersChanged));
|
|
}
|
|
}
|
|
}
|
|
|
|
public override void WorkspaceProjectRemoved(Project workspaceProject)
|
|
{
|
|
if (workspaceProject == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(workspaceProject));
|
|
}
|
|
|
|
_foregroundDispatcher.AssertForegroundThread();
|
|
|
|
if (!IsSupportedWorkspaceProject(workspaceProject))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (_projects.TryGetValue(workspaceProject.FilePath, out var original))
|
|
{
|
|
// We also need to check the projectId here. If this is a multi-targeting project then we are only interested
|
|
// in a single workspace project. Make sure the WorkspaceProject we're using is the one that's being removed.
|
|
if (original.WorkspaceProject?.Id != workspaceProject.Id)
|
|
{
|
|
return;
|
|
}
|
|
|
|
DefaultProjectSnapshot snapshot;
|
|
|
|
// 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.
|
|
//
|
|
// Doing an update to the project should keep computed values, but mark the project as dirty if the
|
|
// underlying project is newer.
|
|
snapshot = original.WithWorkspaceProject(otherWorkspaceProject);
|
|
_projects[workspaceProject.FilePath] = snapshot;
|
|
|
|
if (snapshot.IsInitialized && snapshot.IsDirty)
|
|
{
|
|
// We don't need to notify listeners yet because we don't have any **new** computed state. However we do
|
|
// need to trigger the background work to asynchronously compute the effect of the updates.
|
|
NotifyBackgroundWorker(snapshot.CreateUpdateContext());
|
|
}
|
|
|
|
// Notify listeners of a change because it's a different WorkspaceProject.
|
|
NotifyListeners(new ProjectChangeEventArgs(snapshot, ProjectChangeKind.Changed));
|
|
|
|
return;
|
|
}
|
|
|
|
snapshot = original.RemoveWorkspaceProject();
|
|
_projects[workspaceProject.FilePath] = snapshot;
|
|
|
|
// Notify listeners of a change because we've removed computed state.
|
|
NotifyListeners(new ProjectChangeEventArgs(snapshot, ProjectChangeKind.Changed));
|
|
}
|
|
}
|
|
|
|
public override void ReportError(Exception exception)
|
|
{
|
|
if (exception == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(exception));
|
|
}
|
|
|
|
_errorReporter.ReportError(exception);
|
|
}
|
|
|
|
public override void ReportError(Exception exception, ProjectSnapshot project)
|
|
{
|
|
if (exception == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(exception));
|
|
}
|
|
|
|
_errorReporter.ReportError(exception, project);
|
|
}
|
|
|
|
public override void ReportError(Exception exception, HostProject hostProject)
|
|
{
|
|
if (exception == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(exception));
|
|
}
|
|
|
|
var project = hostProject?.FilePath == null ? null : this.GetProjectWithFilePath(hostProject.FilePath);
|
|
_errorReporter.ReportError(exception, project);
|
|
}
|
|
|
|
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 NotifyBackgroundWorker(ProjectSnapshotUpdateContext context)
|
|
{
|
|
_foregroundDispatcher.AssertForegroundThread();
|
|
|
|
_workerQueue.Enqueue(context);
|
|
}
|
|
|
|
// virtual so it can be overridden in tests
|
|
protected virtual void NotifyListeners(ProjectChangeEventArgs e)
|
|
{
|
|
_foregroundDispatcher.AssertForegroundThread();
|
|
|
|
var handler = Changed;
|
|
if (handler != null)
|
|
{
|
|
handler(this, e);
|
|
}
|
|
}
|
|
}
|
|
} |