diff --git a/src/Microsoft.AspNetCore.Razor.Language/RazorConfiguration.cs b/src/Microsoft.AspNetCore.Razor.Language/RazorConfiguration.cs index e8ef287c0c..1094b0a432 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/RazorConfiguration.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/RazorConfiguration.cs @@ -4,10 +4,11 @@ using System; using System.Collections.Generic; using System.Linq; +using Microsoft.Extensions.Internal; namespace Microsoft.AspNetCore.Razor.Language { - public abstract class RazorConfiguration + public abstract class RazorConfiguration : IEquatable { public static readonly RazorConfiguration Default = new DefaultRazorConfiguration( RazorLanguageVersion.Latest, @@ -43,6 +44,58 @@ namespace Microsoft.AspNetCore.Razor.Language public abstract RazorLanguageVersion LanguageVersion { get; } + public override bool Equals(object obj) + { + return base.Equals(obj as RazorConfiguration); + } + + public virtual bool Equals(RazorConfiguration other) + { + if (object.ReferenceEquals(other, null)) + { + return false; + } + + if (LanguageVersion != other.LanguageVersion) + { + return false; + } + + if (ConfigurationName != other.ConfigurationName) + { + return false; + } + + if (Extensions.Count != other.Extensions.Count) + { + return false; + } + + for (var i = 0; i < Extensions.Count; i++) + { + if (Extensions[i].ExtensionName != other.Extensions[i].ExtensionName) + { + return false; + } + } + + return true; + } + + public override int GetHashCode() + { + var hash = new HashCodeCombiner(); + hash.Add(LanguageVersion); + hash.Add(ConfigurationName); + + for (var i = 0; i < Extensions.Count; i++) + { + hash.Add(Extensions[i].ExtensionName); + } + + return hash; + } + private class DefaultRazorConfiguration : RazorConfiguration { public DefaultRazorConfiguration( diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/DefaultProjectSnapshotProjectEngineFactory.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/DefaultProjectSnapshotProjectEngineFactory.cs new file mode 100644 index 0000000000..cd9ac1ee51 --- /dev/null +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/DefaultProjectSnapshotProjectEngineFactory.cs @@ -0,0 +1,102 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.CodeAnalysis.Razor.ProjectSystem; + +namespace Microsoft.CodeAnalysis.Razor +{ + internal class DefaultProjectSnapshotProjectEngineFactory : ProjectSnapshotProjectEngineFactory + { + private readonly static RazorConfiguration DefaultConfiguration = FallbackRazorConfiguration.MVC_2_1; + + private readonly IFallbackProjectEngineFactory _fallback; + private readonly Lazy[] _factories; + + public DefaultProjectSnapshotProjectEngineFactory( + IFallbackProjectEngineFactory fallback, + Lazy[] factories) + { + if (fallback == null) + { + throw new ArgumentNullException(nameof(fallback)); + } + + if (factories == null) + { + throw new ArgumentNullException(nameof(factories)); + } + + _fallback = fallback; + _factories = factories; + } + + public override RazorProjectEngine Create(ProjectSnapshot project, RazorProjectFileSystem fileSystem, Action configure) + { + if (project == null) + { + throw new ArgumentNullException(nameof(project)); + } + + if (fileSystem == null) + { + throw new ArgumentNullException(nameof(fileSystem)); + } + + // When we're running in the editor, the editor provides a configure delegate that will include + // the editor settings and tag helpers. + // + // This service is only used in process in Visual Studio, and any other callers should provide these + // things also. + configure = configure ?? ((b) => { }); + + // The default configuration currently matches the newest MVC configuration. + // + // We typically want this because the language adds features over time - we don't want to a bunch of errors + // to show up when a document is first opened, and then go away when the configuration loads, we'd prefer the opposite. + var configuration = project.Configuration ?? DefaultConfiguration; + + // If there's no factory to handle the configuration then fall back to a very basic configuration. + // + // This will stop a crash from happening in this case (misconfigured project), but will still make + // it obvious to the user that something is wrong. + var factory = SelectFactory(configuration) ?? _fallback; + return factory.Create(configuration, fileSystem, configure); + } + + public override IProjectEngineFactory FindFactory(ProjectSnapshot project) + { + if (project == null) + { + throw new ArgumentNullException(nameof(project)); + } + + return SelectFactory(project.Configuration ?? DefaultConfiguration, requireSerializable: false); + } + + public override IProjectEngineFactory FindSerializableFactory(ProjectSnapshot project) + { + if (project == null) + { + throw new ArgumentNullException(nameof(project)); + } + + return SelectFactory(project.Configuration ?? DefaultConfiguration, requireSerializable: true); + } + + private IProjectEngineFactory SelectFactory(RazorConfiguration configuration, bool requireSerializable = false) + { + for (var i = 0; i < _factories.Length; i++) + { + var factory = _factories[i]; + if (string.Equals(configuration.ConfigurationName, factory.Metadata.ConfigurationName)) + { + return requireSerializable && !factory.Metadata.SupportsSerialization ? null : factory.Value; + } + } + + return null; + } + } +} diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/DefaultProjectSnapshotProjectEngineFactoryFactory.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/DefaultProjectSnapshotProjectEngineFactoryFactory.cs new file mode 100644 index 0000000000..663bd8f2bc --- /dev/null +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/DefaultProjectSnapshotProjectEngineFactoryFactory.cs @@ -0,0 +1,46 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Composition; +using Microsoft.CodeAnalysis.Host; +using Microsoft.CodeAnalysis.Host.Mef; + +namespace Microsoft.CodeAnalysis.Razor.Workspaces +{ + [ExportWorkspaceServiceFactory(typeof(ProjectSnapshotProjectEngineFactory))] + internal class DefaultProjectSnapshotProjectEngineFactoryFactory : IWorkspaceServiceFactory + { + private readonly IFallbackProjectEngineFactory _fallback; + private readonly Lazy[] _factories; + + [ImportingConstructor] + public DefaultProjectSnapshotProjectEngineFactoryFactory( + IFallbackProjectEngineFactory fallback, + [ImportMany] Lazy[] factories) + { + if (fallback == null) + { + throw new ArgumentNullException(nameof(fallback)); + } + + if (factories == null) + { + throw new ArgumentNullException(nameof(factories)); + } + + _fallback = fallback; + _factories = factories; + } + + public IWorkspaceService CreateService(HostWorkspaceServices workspaceServices) + { + if (workspaceServices == null) + { + throw new ArgumentNullException(nameof(workspaceServices)); + } + + return new DefaultProjectSnapshotProjectEngineFactory(_fallback, _factories); + } + } +} diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/DocumentGenerator/BackgroundDocumentGenerator.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/DocumentGenerator/BackgroundDocumentGenerator.cs new file mode 100644 index 0000000000..6fd591d1ca --- /dev/null +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/DocumentGenerator/BackgroundDocumentGenerator.cs @@ -0,0 +1,278 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Composition; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Razor.ProjectSystem; +using Microsoft.Extensions.Internal; + +namespace Microsoft.CodeAnalysis.Razor +{ + // Deliberately not exported for now, until this feature is working end to end. + internal class BackgroundDocumentGenerator : ProjectSnapshotChangeTrigger + { + private ForegroundDispatcher _foregroundDispatcher; + private ProjectSnapshotManagerBase _projectManager; + + private readonly Dictionary _files; + private Timer _timer; + + [ImportingConstructor] + public BackgroundDocumentGenerator(ForegroundDispatcher foregroundDispatcher) + { + if (foregroundDispatcher == null) + { + throw new ArgumentNullException(nameof(foregroundDispatcher)); + } + + _foregroundDispatcher = foregroundDispatcher; + + _files = new Dictionary(); + } + + public bool HasPendingNotifications + { + get + { + lock (_files) + { + return _files.Count > 0; + } + } + } + + // Used in unit tests to control the timer delay. + public TimeSpan Delay { get; set; } = TimeSpan.FromSeconds(2); + + public bool IsScheduledOrRunning => _timer != null; + + // Used in unit tests to ensure we can control when background work starts. + public ManualResetEventSlim BlockBackgroundWorkStart { get; set; } + + // Used in unit tests to ensure we can know when background work finishes. + public ManualResetEventSlim NotifyBackgroundWorkStarting { get; set; } + + // Used in unit tests to ensure we can control when background work completes. + public ManualResetEventSlim BlockBackgroundWorkCompleting { get; set; } + + // Used in unit tests to ensure we can know when background work finishes. + public ManualResetEventSlim NotifyBackgroundWorkCompleted { get; set; } + + private void OnStartingBackgroundWork() + { + if (BlockBackgroundWorkStart != null) + { + BlockBackgroundWorkStart.Wait(); + BlockBackgroundWorkStart.Reset(); + } + + if (NotifyBackgroundWorkStarting != null) + { + NotifyBackgroundWorkStarting.Set(); + } + } + + private void OnCompletingBackgroundWork() + { + if (BlockBackgroundWorkCompleting != null) + { + BlockBackgroundWorkCompleting.Wait(); + BlockBackgroundWorkCompleting.Reset(); + } + } + + private void OnCompletedBackgroundWork() + { + if (NotifyBackgroundWorkCompleted != null) + { + NotifyBackgroundWorkCompleted.Set(); + } + } + + public override void Initialize(ProjectSnapshotManagerBase projectManager) + { + if (projectManager == null) + { + throw new ArgumentNullException(nameof(projectManager)); + } + + _projectManager = projectManager; + _projectManager.Changed += ProjectManager_Changed; + } + + protected virtual Task ProcessDocument(DocumentSnapshot document) + { + return document.GetGeneratedOutputAsync(); + } + + public void Enqueue(ProjectSnapshot project, DocumentSnapshot document) + { + if (project == null) + { + throw new ArgumentNullException(nameof(project)); + } + + if (document == null) + { + throw new ArgumentNullException(nameof(document)); + } + + _foregroundDispatcher.AssertForegroundThread(); + + lock (_files) + { + // We only want to store the last 'seen' version of any given document. That way when we pick one to process + // it's always the best version to use. + _files.Add(new Key(project.FilePath, document.FilePath), document); + + StartWorker(); + } + } + + protected virtual void StartWorker() + { + // Access to the timer is protected by the lock in Enqueue and in Timer_Tick + if (_timer == null) + { + // Timer will fire after a fixed delay, but only once. + _timer = new Timer(Timer_Tick, null, Delay, Timeout.InfiniteTimeSpan); + } + } + + private async void Timer_Tick(object state) // Yeah I know. + { + try + { + _foregroundDispatcher.AssertBackgroundThread(); + + // Timer is stopped. + _timer.Change(Timeout.Infinite, Timeout.Infinite); + + OnStartingBackgroundWork(); + + DocumentSnapshot[] work; + lock (_files) + { + work = _files.Values.ToArray(); + _files.Clear(); + } + + for (var i = 0; i < work.Length; i++) + { + var document = work[i]; + try + { + await ProcessDocument(document); + } + catch (Exception ex) + { + ReportError(document, ex); + } + } + + OnCompletingBackgroundWork(); + + lock (_files) + { + // Resetting the timer allows another batch of work to start. + _timer.Dispose(); + _timer = null; + + // If more work came in while we were running start the worker again. + if (_files.Count > 0) + { + StartWorker(); + } + } + + OnCompletedBackgroundWork(); + } + catch (Exception ex) + { + // This is something totally unexpected, let's just send it over to the workspace. + await Task.Factory.StartNew( + () => _projectManager.ReportError(ex), + CancellationToken.None, + TaskCreationOptions.None, + _foregroundDispatcher.ForegroundScheduler); + } + } + + private void ReportError(DocumentSnapshot document, Exception ex) + { + GC.KeepAlive(Task.Factory.StartNew( + () => _projectManager.ReportError(ex), + CancellationToken.None, + TaskCreationOptions.None, + _foregroundDispatcher.ForegroundScheduler)); + } + + private void ProjectManager_Changed(object sender, ProjectChangeEventArgs e) + { + switch (e.Kind) + { + case ProjectChangeKind.ProjectAdded: + case ProjectChangeKind.ProjectChanged: + case ProjectChangeKind.DocumentsChanged: + { + var project = _projectManager.GetLoadedProject(e.ProjectFilePath); + foreach (var documentFilePath in project.DocumentFilePaths) + { + Enqueue(project, project.GetDocument(documentFilePath)); + } + + break; + } + + case ProjectChangeKind.DocumentContentChanged: + { + throw null; + } + + case ProjectChangeKind.ProjectRemoved: + // ignore + break; + + default: + throw new InvalidOperationException($"Unknown ProjectChangeKind {e.Kind}"); + } + } + + private struct Key : IEquatable + { + public Key(string projectFilePath, string documentFilePath) + { + ProjectFilePath = projectFilePath; + DocumentFilePath = documentFilePath; + } + + public string ProjectFilePath { get; } + + public string DocumentFilePath { get; } + + public bool Equals(Key other) + { + return + FilePathComparer.Instance.Equals(ProjectFilePath, other.ProjectFilePath) && + FilePathComparer.Instance.Equals(DocumentFilePath, other.DocumentFilePath); + } + + public override bool Equals(object obj) + { + return obj is Key key ? Equals(key) : false; + } + + public override int GetHashCode() + { + var hash = new HashCodeCombiner(); + hash.Add(ProjectFilePath, FilePathComparer.Instance); + hash.Add(DocumentFilePath, FilePathComparer.Instance); + return hash; + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSnapshotProjectEngineFactory.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSnapshotProjectEngineFactory.cs new file mode 100644 index 0000000000..cf2341878d --- /dev/null +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSnapshotProjectEngineFactory.cs @@ -0,0 +1,51 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.IO; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.CodeAnalysis.Host; +using Microsoft.CodeAnalysis.Razor.ProjectSystem; + +namespace Microsoft.CodeAnalysis.Razor +{ + internal abstract class ProjectSnapshotProjectEngineFactory : IWorkspaceService + { + public abstract IProjectEngineFactory FindFactory(ProjectSnapshot project); + + public abstract IProjectEngineFactory FindSerializableFactory(ProjectSnapshot project); + + public RazorProjectEngine Create(ProjectSnapshot project) + { + return Create(project, RazorProjectFileSystem.Create(Path.GetDirectoryName(project.FilePath)), null); + } + + public RazorProjectEngine Create(ProjectSnapshot project, RazorProjectFileSystem fileSystem) + { + if (project == null) + { + throw new ArgumentNullException(nameof(project)); + } + + if (fileSystem == null) + { + throw new ArgumentNullException(nameof(fileSystem)); + } + + return Create(project, fileSystem, null); + } + + public RazorProjectEngine Create(ProjectSnapshot project, Action configure) + { + if (project == null) + { + throw new ArgumentNullException(nameof(project)); + } + + return Create(project, RazorProjectFileSystem.Create(Path.GetDirectoryName(project.FilePath)), configure); + } + + public abstract RazorProjectEngine Create(ProjectSnapshot project, RazorProjectFileSystem fileSystem, Action configure); + + } +} diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultDocumentSnapshot.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultDocumentSnapshot.cs new file mode 100644 index 0000000000..eb1c8be640 --- /dev/null +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultDocumentSnapshot.cs @@ -0,0 +1,54 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Razor.Language; + +namespace Microsoft.CodeAnalysis.Razor.ProjectSystem +{ + internal class DefaultDocumentSnapshot : DocumentSnapshot + { + public DefaultDocumentSnapshot(ProjectSnapshot project, DocumentState state) + { + if (project == null) + { + throw new ArgumentNullException(nameof(project)); + } + + if (state == null) + { + throw new ArgumentNullException(nameof(state)); + } + + Project = project; + State = state; + } + + public ProjectSnapshot Project { get; } + + public DocumentState State { get; } + + public override string FilePath => State.HostDocument.FilePath; + + public override string TargetPath => State.HostDocument.TargetPath; + + public override Task GetGeneratedOutputAsync() + { + // IMPORTANT: Don't put more code here. We want this to return a cached task. + return State.GeneratedOutput.GetGeneratedOutputInitializationTask(Project, this); + } + + public override bool TryGetGeneratedOutput(out RazorCodeDocument results) + { + if (State.GeneratedOutput.IsResultAvailable) + { + results = State.GeneratedOutput.GetGeneratedOutputInitializationTask(Project, this).Result; + return true; + } + + results = null; + return false; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultProjectSnapshot.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultProjectSnapshot.cs index b8646429c6..2b644e182c 100644 --- a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultProjectSnapshot.cs +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultProjectSnapshot.cs @@ -3,182 +3,82 @@ using System; using System.Collections.Generic; -using System.Linq; +using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.Language; namespace Microsoft.CodeAnalysis.Razor.ProjectSystem { - // All of the public state of this is immutable - we create a new instance and notify subscribers - // when it changes. - // - // However we use the private state to track things like dirty/clean. - // - // See the private constructors... When we update the snapshot we either are processing a Workspace - // change (Project) or updating the computed state (ProjectSnapshotUpdateContext). We don't do both - // at once. internal class DefaultProjectSnapshot : ProjectSnapshot { - public DefaultProjectSnapshot(HostProject hostProject, Project workspaceProject, VersionStamp? version = null) + private readonly object _lock; + + private Dictionary _documents; + + public DefaultProjectSnapshot(ProjectState state) { - if (hostProject == null) + if (state == null) { - throw new ArgumentNullException(nameof(hostProject)); + throw new ArgumentNullException(nameof(state)); } - HostProject = hostProject; - WorkspaceProject = workspaceProject; // Might be null - - FilePath = hostProject.FilePath; - Version = version ?? VersionStamp.Default; + State = state; + + _lock = new object(); + _documents = new Dictionary(FilePathComparer.Instance); } - private DefaultProjectSnapshot(HostProject hostProject, DefaultProjectSnapshot other) - { - if (hostProject == null) - { - throw new ArgumentNullException(nameof(hostProject)); - } - - if (other == null) - { - throw new ArgumentNullException(nameof(other)); - } - - ComputedVersion = other.ComputedVersion; - - FilePath = other.FilePath; - TagHelpers = other.TagHelpers; - HostProject = hostProject; - WorkspaceProject = other.WorkspaceProject; - - Version = other.Version.GetNewerVersion(); - } - - private DefaultProjectSnapshot(Project workspaceProject, DefaultProjectSnapshot other) - { - if (workspaceProject == null) - { - throw new ArgumentNullException(nameof(workspaceProject)); - } - - if (other == null) - { - throw new ArgumentNullException(nameof(other)); - } - - ComputedVersion = other.ComputedVersion; - - FilePath = other.FilePath; - TagHelpers = other.TagHelpers; - HostProject = other.HostProject; - WorkspaceProject = workspaceProject; - - Version = other.Version.GetNewerVersion(); - } - - private DefaultProjectSnapshot(ProjectSnapshotUpdateContext update, DefaultProjectSnapshot other) - { - if (update == null) - { - throw new ArgumentNullException(nameof(update)); - } - - if (other == null) - { - throw new ArgumentNullException(nameof(other)); - } - - ComputedVersion = update.Version; - - FilePath = other.FilePath; - HostProject = other.HostProject; - TagHelpers = update.TagHelpers ?? Array.Empty(); - WorkspaceProject = other.WorkspaceProject; - - // This doesn't represent a new version of the underlying data. Keep the same version. - Version = other.Version; - } + public ProjectState State { get; } public override RazorConfiguration Configuration => HostProject.Configuration; - public override string FilePath { get; } + public override IEnumerable DocumentFilePaths => State.Documents.Keys; - public override HostProject HostProject { get; } + public override string FilePath => State.HostProject.FilePath; + + public HostProject HostProject => State.HostProject; public override bool IsInitialized => WorkspaceProject != null; - public override VersionStamp Version { get; } + public override VersionStamp Version => State.Version; - public override Project WorkspaceProject { get; } + public override Project WorkspaceProject => State.WorkspaceProject; - public override IReadOnlyList TagHelpers { get; } = Array.Empty(); - - // This is the version that the computed state is based on. - public VersionStamp? ComputedVersion { get; set; } - - // We know the project is dirty if we don't have a computed result, or it was computed for a different version. - // Since the PSM updates the snapshots synchronously, the snapshot can never be older than the computed state. - public bool IsDirty => ComputedVersion == null || ComputedVersion.Value != Version; - - public ProjectSnapshotUpdateContext CreateUpdateContext() + public override DocumentSnapshot GetDocument(string filePath) { - return new ProjectSnapshotUpdateContext(FilePath, HostProject, WorkspaceProject, Version); + lock (_lock) + { + if (!_documents.TryGetValue(filePath, out var result) && + State.Documents.TryGetValue(filePath, out var state)) + { + result = new DefaultDocumentSnapshot(this, state); + _documents.Add(filePath, result); + } + + return result; + } } - public DefaultProjectSnapshot WithHostProject(HostProject hostProject) + public override RazorProjectEngine GetProjectEngine() { - if (hostProject == null) + return State.ProjectEngine.GetProjectEngine(this); + } + + public override Task> GetTagHelpersAsync() + { + // IMPORTANT: Don't put more code here. We want this to return a cached task. + return State.TagHelpers.GetTagHelperInitializationTask(this); + } + + public override bool TryGetTagHelpers(out IReadOnlyList results) + { + if (State.TagHelpers.IsResultAvailable) { - throw new ArgumentNullException(nameof(hostProject)); + results = State.TagHelpers.GetTagHelperInitializationTask(this).Result; + return true; } - return new DefaultProjectSnapshot(hostProject, this); - } - - public DefaultProjectSnapshot RemoveWorkspaceProject() - { - // We want to get rid of all of the computed state since it's not really valid. - return new DefaultProjectSnapshot(HostProject, null, Version.GetNewerVersion()); - } - - public DefaultProjectSnapshot WithWorkspaceProject(Project workspaceProject) - { - if (workspaceProject == null) - { - throw new ArgumentNullException(nameof(workspaceProject)); - } - - return new DefaultProjectSnapshot(workspaceProject, this); - } - - public DefaultProjectSnapshot WithComputedUpdate(ProjectSnapshotUpdateContext update) - { - if (update == null) - { - throw new ArgumentNullException(nameof(update)); - } - - return new DefaultProjectSnapshot(update, this); - } - - public bool HasConfigurationChanged(DefaultProjectSnapshot original) - { - if (original == null) - { - throw new ArgumentNullException(nameof(original)); - } - - return !object.Equals(Configuration, original.Configuration); - } - - public bool HaveTagHelpersChanged(ProjectSnapshot original) - { - if (original == null) - { - throw new ArgumentNullException(nameof(original)); - } - - return !Enumerable.SequenceEqual(TagHelpers, original.TagHelpers); + results = null; + return false; } } } \ No newline at end of file diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultProjectSnapshotManager.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultProjectSnapshotManager.cs index c246fa14d7..8c3a5484a0 100644 --- a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultProjectSnapshotManager.cs +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultProjectSnapshotManager.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; namespace Microsoft.CodeAnalysis.Razor.ProjectSystem @@ -31,15 +30,14 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem private readonly ErrorReporter _errorReporter; private readonly ForegroundDispatcher _foregroundDispatcher; private readonly ProjectSnapshotChangeTrigger[] _triggers; - private readonly ProjectSnapshotWorkerQueue _workerQueue; - private readonly ProjectSnapshotWorker _worker; - private readonly Dictionary _projects; + // Each entry holds a ProjectState and an optional ProjectSnapshot. ProjectSnapshots are + // created lazily. + private readonly Dictionary _projects; public DefaultProjectSnapshotManager( ForegroundDispatcher foregroundDispatcher, ErrorReporter errorReporter, - ProjectSnapshotWorker worker, IEnumerable triggers, Workspace workspace) { @@ -53,11 +51,6 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem throw new ArgumentNullException(nameof(errorReporter)); } - if (worker == null) - { - throw new ArgumentNullException(nameof(worker)); - } - if (triggers == null) { throw new ArgumentNullException(nameof(triggers)); @@ -70,13 +63,10 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem _foregroundDispatcher = foregroundDispatcher; _errorReporter = errorReporter; - _worker = worker; _triggers = triggers.ToArray(); Workspace = workspace; - _projects = new Dictionary(FilePathComparer.Instance); - - _workerQueue = new ProjectSnapshotWorkerQueue(_foregroundDispatcher, this, worker); + _projects = new Dictionary(FilePathComparer.Instance); for (var i = 0; i < _triggers.Length; i++) { @@ -89,43 +79,109 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem get { _foregroundDispatcher.AssertForegroundThread(); - return _projects.Values.ToArray(); + + + var i = 0; + var projects = new ProjectSnapshot[_projects.Count]; + foreach (var entry in _projects) + { + if (entry.Value.Snapshot == null) + { + entry.Value.Snapshot = new DefaultProjectSnapshot(entry.Value.State); + } + + projects[i++] = entry.Value.Snapshot; + } + + return projects; } } public override Workspace Workspace { get; } - public override void ProjectUpdated(ProjectSnapshotUpdateContext update) + public override ProjectSnapshot GetLoadedProject(string filePath) { - if (update == null) + if (filePath == null) { - throw new ArgumentNullException(nameof(update)); + throw new ArgumentNullException(nameof(filePath)); } _foregroundDispatcher.AssertForegroundThread(); - if (_projects.TryGetValue(update.WorkspaceProject.FilePath, out var original)) + if (_projects.TryGetValue(filePath, out var entry)) { - if (!original.IsInitialized) + if (entry.Snapshot == null) { - // If the project has been uninitialized, just ignore the update. - return; + entry.Snapshot = new DefaultProjectSnapshot(entry.State); } - // This is an update to the project's computed values, so everything should be overwritten - var snapshot = original.WithComputedUpdate(update); - _projects[update.WorkspaceProject.FilePath] = snapshot; + return entry.Snapshot; + } - if (snapshot.IsDirty) + return null; + } + + public override ProjectSnapshot GetOrCreateProject(string filePath) + { + if (filePath == null) + { + throw new ArgumentNullException(nameof(filePath)); + } + + _foregroundDispatcher.AssertForegroundThread(); + + return GetLoadedProject(filePath) ?? new EphemeralProjectSnapshot(Workspace.Services, filePath); + } + + public override void DocumentAdded(HostProject hostProject, HostDocument document) + { + if (hostProject == null) + { + throw new ArgumentNullException(nameof(hostProject)); + } + + if (document == null) + { + throw new ArgumentNullException(nameof(document)); + } + + _foregroundDispatcher.AssertForegroundThread(); + + if (_projects.TryGetValue(hostProject.FilePath, out var entry)) + { + var state = entry.State.AddHostDocument(document); + + // Document updates can no-op. + if (!object.ReferenceEquals(state, entry.State)) { - // It's possible that the snapshot can still be dirty if we got a project update while computing state in - // the background. We need to trigger the background work to asynchronously compute the effect of the updates. - NotifyBackgroundWorker(snapshot.CreateUpdateContext()); + _projects[hostProject.FilePath] = new Entry(state); + NotifyListeners(new ProjectChangeEventArgs(hostProject.FilePath, ProjectChangeKind.DocumentsChanged)); } - - if (!object.Equals(snapshot.ComputedVersion, original.ComputedVersion)) + } + } + + public override void DocumentRemoved(HostProject hostProject, HostDocument document) + { + if (hostProject == null) + { + throw new ArgumentNullException(nameof(hostProject)); + } + + if (document == null) + { + throw new ArgumentNullException(nameof(document)); + } + + _foregroundDispatcher.AssertForegroundThread(); + if (_projects.TryGetValue(hostProject.FilePath, out var entry)) + { + var state = entry.State.RemoveHostDocument(document); + + // Document updates can no-op. + if (!object.ReferenceEquals(state, entry.State)) { - NotifyListeners(new ProjectChangeEventArgs(snapshot, ProjectChangeKind.TagHelpersChanged)); + _projects[hostProject.FilePath] = new Entry(state); + NotifyListeners(new ProjectChangeEventArgs(hostProject.FilePath, ProjectChangeKind.DocumentsChanged)); } } } @@ -149,17 +205,11 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem // So if possible find a WorkspaceProject. var workspaceProject = GetWorkspaceProject(hostProject.FilePath); - var snapshot = new DefaultProjectSnapshot(hostProject, workspaceProject); - _projects[hostProject.FilePath] = snapshot; - - if (snapshot.IsInitialized && snapshot.IsDirty) - { - // Start computing background state if the project is fully initialized. - NotifyBackgroundWorker(snapshot.CreateUpdateContext()); - } + var state = new ProjectState(Workspace.Services, hostProject, workspaceProject); + _projects[hostProject.FilePath] = new Entry(state); // We need to notify listeners about every project add. - NotifyListeners(new ProjectChangeEventArgs(snapshot, ProjectChangeKind.Added)); + NotifyListeners(new ProjectChangeEventArgs(hostProject.FilePath, ProjectChangeKind.ProjectAdded)); } public override void HostProjectChanged(HostProject hostProject) @@ -171,22 +221,17 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem _foregroundDispatcher.AssertForegroundThread(); - if (_projects.TryGetValue(hostProject.FilePath, out var original)) + if (_projects.TryGetValue(hostProject.FilePath, out var entry)) { - // Doing an update to the project should keep computed values, but mark the project as dirty if the - // underlying project is newer. - var snapshot = original.WithHostProject(hostProject); - _projects[hostProject.FilePath] = snapshot; + var state = entry.State.WithHostProject(hostProject); - if (snapshot.IsInitialized && snapshot.IsDirty) + // HostProject updates can no-op. + if (!object.ReferenceEquals(state, entry.State)) { - // Start computing background state if the project is fully initialized. - NotifyBackgroundWorker(snapshot.CreateUpdateContext()); - } + _projects[hostProject.FilePath] = new Entry(state); - // Notify listeners right away because if the HostProject changes then it's likely that the Razor - // configuration changed. - NotifyListeners(new ProjectChangeEventArgs(snapshot, ProjectChangeKind.Changed)); + NotifyListeners(new ProjectChangeEventArgs(hostProject.FilePath, ProjectChangeKind.ProjectChanged)); + } } } @@ -204,37 +249,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem _projects.Remove(hostProject.FilePath); // We need to notify listeners about every project removal. - NotifyListeners(new ProjectChangeEventArgs(snapshot, ProjectChangeKind.Removed)); - } - } - - public override void HostProjectBuildComplete(HostProject hostProject) - { - if (hostProject == null) - { - throw new ArgumentNullException(nameof(hostProject)); - } - - _foregroundDispatcher.AssertForegroundThread(); - - if (_projects.TryGetValue(hostProject.FilePath, out var original)) - { - var workspaceProject = GetWorkspaceProject(hostProject.FilePath); - if (workspaceProject == null) - { - // Host project was built prior to a workspace project being associated. We have nothing to do without - // a workspace project so we short circuit. - return; - } - - // Doing an update to the project should keep computed values, but mark the project as dirty if the - // underlying project is newer. - var snapshot = original.WithWorkspaceProject(workspaceProject); - - _projects[hostProject.FilePath] = snapshot; - - // Notify the background worker so it can trigger tag helper discovery. - NotifyBackgroundWorker(snapshot.CreateUpdateContext()); + NotifyListeners(new ProjectChangeEventArgs(hostProject.FilePath, ProjectChangeKind.ProjectRemoved)); } } @@ -254,25 +269,16 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem // The WorkspaceProject initialization never triggers a "Project Add" from out point of view, we // only care if the new WorkspaceProject matches an existing HostProject. - if (_projects.TryGetValue(workspaceProject.FilePath, out var original)) + if (_projects.TryGetValue(workspaceProject.FilePath, out var entry)) { // If this is a multi-targeting project then we are only interested in a single workspace project. If we already // found one in the past just ignore this one. - if (original.WorkspaceProject == null) + if (entry.State.WorkspaceProject == null) { - var snapshot = original.WithWorkspaceProject(workspaceProject); - _projects[workspaceProject.FilePath] = snapshot; + var state = entry.State.WithWorkspaceProject(workspaceProject); + _projects[workspaceProject.FilePath] = new Entry(state); - if (snapshot.IsInitialized && snapshot.IsDirty) - { - // We don't need to notify listeners yet because we don't have any **new** computed state. - // - // However we do need to trigger the background work to asynchronously compute the effect of the updates. - NotifyBackgroundWorker(snapshot.CreateUpdateContext()); - } - - // Notify listeners right away since WorkspaceProject was just added, the project is now initialized. - NotifyListeners(new ProjectChangeEventArgs(snapshot, ProjectChangeKind.Changed)); + NotifyListeners(new ProjectChangeEventArgs(workspaceProject.FilePath, ProjectChangeKind.ProjectChanged)); } } } @@ -293,25 +299,18 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem // We also need to check the projectId here. If this is a multi-targeting project then we are only interested // in a single workspace project. Just use the one that showed up first. - if (_projects.TryGetValue(workspaceProject.FilePath, out var original) && - (original.WorkspaceProject == null || - original.WorkspaceProject.Id == workspaceProject.Id)) + if (_projects.TryGetValue(workspaceProject.FilePath, out var entry) && + (entry.State.WorkspaceProject == null || entry.State.WorkspaceProject.Id == workspaceProject.Id)) { - // Doing an update to the project should keep computed values, but mark the project as dirty if the - // underlying project is newer. - var snapshot = original.WithWorkspaceProject(workspaceProject); - _projects[workspaceProject.FilePath] = snapshot; - - if (snapshot.IsInitialized && snapshot.IsDirty) + var state = entry.State.WithWorkspaceProject(workspaceProject); + + // WorkspaceProject updates can no-op. This can be the case if a build is triggered, but we've + // already seen the update. + if (!object.ReferenceEquals(state, entry.State)) { - // We don't need to notify listeners yet because we don't have any **new** computed state. However we do - // need to trigger the background work to asynchronously compute the effect of the updates. - NotifyBackgroundWorker(snapshot.CreateUpdateContext()); - } + _projects[workspaceProject.FilePath] = new Entry(state); - if (snapshot.HaveTagHelpersChanged(original)) - { - NotifyListeners(new ProjectChangeEventArgs(snapshot, ProjectChangeKind.TagHelpersChanged)); + NotifyListeners(new ProjectChangeEventArgs(workspaceProject.FilePath, ProjectChangeKind.ProjectChanged)); } } } @@ -330,16 +329,16 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem return; } - if (_projects.TryGetValue(workspaceProject.FilePath, out var original)) + if (_projects.TryGetValue(workspaceProject.FilePath, out var entry)) { // We also need to check the projectId here. If this is a multi-targeting project then we are only interested // in a single workspace project. Make sure the WorkspaceProject we're using is the one that's being removed. - if (original.WorkspaceProject?.Id != workspaceProject.Id) + if (entry.State.WorkspaceProject?.Id != workspaceProject.Id) { return; } - DefaultProjectSnapshot snapshot; + ProjectState state; // So if the WorkspaceProject got removed, we should double check to make sure that there aren't others // hanging around. This could happen if a project is multi-targeting and one of the TFMs is removed. @@ -347,30 +346,19 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem if (otherWorkspaceProject != null && otherWorkspaceProject.Id != workspaceProject.Id) { // OK there's another WorkspaceProject, use that. - // - // Doing an update to the project should keep computed values, but mark the project as dirty if the - // underlying project is newer. - snapshot = original.WithWorkspaceProject(otherWorkspaceProject); - _projects[workspaceProject.FilePath] = snapshot; + state = entry.State.WithWorkspaceProject(otherWorkspaceProject); + _projects[otherWorkspaceProject.FilePath] = new Entry(state); - if (snapshot.IsInitialized && snapshot.IsDirty) - { - // We don't need to notify listeners yet because we don't have any **new** computed state. However we do - // need to trigger the background work to asynchronously compute the effect of the updates. - NotifyBackgroundWorker(snapshot.CreateUpdateContext()); - } - - // Notify listeners of a change because it's a different WorkspaceProject. - NotifyListeners(new ProjectChangeEventArgs(snapshot, ProjectChangeKind.Changed)); - - return; + NotifyListeners(new ProjectChangeEventArgs(otherWorkspaceProject.FilePath, ProjectChangeKind.ProjectChanged)); } + else + { + state = entry.State.WithWorkspaceProject(null); + _projects[workspaceProject.FilePath] = new Entry(state); - snapshot = original.RemoveWorkspaceProject(); - _projects[workspaceProject.FilePath] = snapshot; - - // Notify listeners of a change because we've removed computed state. - NotifyListeners(new ProjectChangeEventArgs(snapshot, ProjectChangeKind.Changed)); + // Notify listeners of a change because we've removed computed state. + NotifyListeners(new ProjectChangeEventArgs(workspaceProject.FilePath, ProjectChangeKind.ProjectChanged)); + } } } @@ -401,8 +389,8 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem throw new ArgumentNullException(nameof(exception)); } - var project = hostProject?.FilePath == null ? null : this.GetProjectWithFilePath(hostProject.FilePath); - _errorReporter.ReportError(exception, project); + var snapshot = hostProject?.FilePath == null ? null : GetLoadedProject(hostProject.FilePath); + _errorReporter.ReportError(exception, snapshot); } public override void ReportError(Exception exception, Project workspaceProject) @@ -411,7 +399,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem { throw new ArgumentNullException(nameof(exception)); } - + _errorReporter.ReportError(exception, workspaceProject); } @@ -440,14 +428,6 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem return null; } - // virtual so it can be overridden in tests - protected virtual void NotifyBackgroundWorker(ProjectSnapshotUpdateContext context) - { - _foregroundDispatcher.AssertForegroundThread(); - - _workerQueue.Enqueue(context); - } - // virtual so it can be overridden in tests protected virtual void NotifyListeners(ProjectChangeEventArgs e) { @@ -459,5 +439,16 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem handler(this, e); } } + + private class Entry + { + public ProjectSnapshot Snapshot; + public readonly ProjectState State; + + public Entry(ProjectState state) + { + State = state; + } + } } } \ No newline at end of file diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultProjectSnapshotManagerFactory.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultProjectSnapshotManagerFactory.cs index d82d82cd7c..b092174aa6 100644 --- a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultProjectSnapshotManagerFactory.cs +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultProjectSnapshotManagerFactory.cs @@ -45,8 +45,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem return new DefaultProjectSnapshotManager( _foregroundDispatcher, languageServices.WorkspaceServices.GetRequiredService(), - languageServices.GetRequiredService(), - _triggers, + _triggers, languageServices.WorkspaceServices.Workspace); } } diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultProjectSnapshotWorker.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultProjectSnapshotWorker.cs deleted file mode 100644 index 1b7480f940..0000000000 --- a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultProjectSnapshotWorker.cs +++ /dev/null @@ -1,62 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.Threading; -using System.Threading.Tasks; - -namespace Microsoft.CodeAnalysis.Razor.ProjectSystem -{ - internal class DefaultProjectSnapshotWorker : ProjectSnapshotWorker - { - private readonly ForegroundDispatcher _foregroundDispatcher; - private readonly TagHelperResolver _tagHelperResolver; - - public DefaultProjectSnapshotWorker(ForegroundDispatcher foregroundDispatcher, TagHelperResolver tagHelperResolver) - { - if (foregroundDispatcher == null) - { - throw new ArgumentNullException(nameof(foregroundDispatcher)); - } - - if (tagHelperResolver == null) - { - throw new ArgumentNullException(nameof(tagHelperResolver)); - } - - _foregroundDispatcher = foregroundDispatcher; - _tagHelperResolver = tagHelperResolver; - } - - public override Task ProcessUpdateAsync(ProjectSnapshotUpdateContext update, CancellationToken cancellationToken = default(CancellationToken)) - { - if (update == null) - { - throw new ArgumentNullException(nameof(update)); - } - - // Don't block the main thread - if (_foregroundDispatcher.IsForegroundThread) - { - return Task.Factory.StartNew(ProjectUpdatesCoreAsync, update, cancellationToken, TaskCreationOptions.None, _foregroundDispatcher.BackgroundScheduler); - } - - return ProjectUpdatesCoreAsync(update); - } - - protected virtual void OnProcessingUpdate() - { - } - - private async Task ProjectUpdatesCoreAsync(object state) - { - var update = (ProjectSnapshotUpdateContext)state; - - OnProcessingUpdate(); - - var snapshot = new DefaultProjectSnapshot(update.HostProject, update.WorkspaceProject, update.Version); - var result = await _tagHelperResolver.GetTagHelpersAsync(snapshot, CancellationToken.None); - update.TagHelpers = result.Descriptors; - } - } -} diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultProjectSnapshotWorkerFactory.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultProjectSnapshotWorkerFactory.cs deleted file mode 100644 index bd36bf361d..0000000000 --- a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultProjectSnapshotWorkerFactory.cs +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System.Composition; -using Microsoft.CodeAnalysis.Host; -using Microsoft.CodeAnalysis.Host.Mef; - -namespace Microsoft.CodeAnalysis.Razor.ProjectSystem -{ - [Shared] - [ExportLanguageServiceFactory(typeof(ProjectSnapshotWorker), RazorLanguage.Name)] - internal class DefaultProjectSnapshotWorkerFactory : ILanguageServiceFactory - { - private readonly ForegroundDispatcher _foregroundDispatcher; - - [ImportingConstructor] - public DefaultProjectSnapshotWorkerFactory(ForegroundDispatcher foregroundDispatcher) - { - if (foregroundDispatcher == null) - { - throw new System.ArgumentNullException(nameof(foregroundDispatcher)); - } - - _foregroundDispatcher = foregroundDispatcher; - } - - public ILanguageService CreateLanguageService(HostLanguageServices languageServices) - { - return new DefaultProjectSnapshotWorker(_foregroundDispatcher, languageServices.GetRequiredService()); - } - } -} diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DocumentGeneratedOutputTracker.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DocumentGeneratedOutputTracker.cs new file mode 100644 index 0000000000..e3114148b1 --- /dev/null +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DocumentGeneratedOutputTracker.cs @@ -0,0 +1,111 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Razor.Language; + +namespace Microsoft.CodeAnalysis.Razor.ProjectSystem +{ + internal class DocumentGeneratedOutputTracker + { + // Don't keep anything if the configuration has changed. It's OK if the + // workspace project has changes, we'll consider whether the tag helpers are + // difference before reusing a previous result. + private const ProjectDifference Mask = ProjectDifference.ConfigurationChanged; + + private readonly object _lock; + + private DocumentGeneratedOutputTracker _older; + private Task _task; + + private IReadOnlyList _tagHelpers; + + public DocumentGeneratedOutputTracker(DocumentGeneratedOutputTracker older) + { + _older = older; + + _lock = new object(); + } + + public bool IsResultAvailable => _task?.IsCompleted == true; + + public Task GetGeneratedOutputInitializationTask(ProjectSnapshot project, DocumentSnapshot document) + { + if (project == null) + { + throw new ArgumentNullException(nameof(project)); + } + + if (document == null) + { + throw new ArgumentNullException(nameof(document)); + } + + if (_task == null) + { + lock (_lock) + { + if (_task == null) + { + _task = GetGeneratedOutputInitializationTaskCore(project, document); + } + } + } + + return _task; + } + + public DocumentGeneratedOutputTracker ForkFor(DocumentState state, ProjectDifference difference) + { + if (state == null) + { + throw new ArgumentNullException(nameof(state)); + } + + if ((difference & Mask) != 0) + { + return null; + } + + return new DocumentGeneratedOutputTracker(this); + } + + private async Task GetGeneratedOutputInitializationTaskCore(ProjectSnapshot project, DocumentSnapshot document) + { + var tagHelpers = await project.GetTagHelpersAsync().ConfigureAwait(false); + if (_older != null && _older.IsResultAvailable) + { + var difference = new HashSet(TagHelperDescriptorComparer.Default); + difference.UnionWith(_older._tagHelpers); + difference.SymmetricExceptWith(tagHelpers); + + if (difference.Count == 0) + { + // We can use the cached result. + var result = _older._task.Result; + + // Drop reference so it can be GC'ed + _older = null; + + // Cache the tag helpers so the next version can use them + _tagHelpers = tagHelpers; + + return result; + } + } + + // Drop reference so it can be GC'ed + _older = null; + + + // Cache the tag helpers so the next version can use them + _tagHelpers = tagHelpers; + + var projectEngine = project.GetProjectEngine(); + var projectItem = projectEngine.FileSystem.GetItem(document.FilePath); + return projectItem == null ? null : projectEngine.ProcessDesignTime(projectItem); + } + } +} diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DocumentSnapshot.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DocumentSnapshot.cs new file mode 100644 index 0000000000..736d09c65c --- /dev/null +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DocumentSnapshot.cs @@ -0,0 +1,19 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Threading.Tasks; +using Microsoft.AspNetCore.Razor.Language; + +namespace Microsoft.CodeAnalysis.Razor.ProjectSystem +{ + internal abstract class DocumentSnapshot + { + public abstract string FilePath { get; } + + public abstract string TargetPath { get; } + + public abstract Task GetGeneratedOutputAsync(); + + public abstract bool TryGetGeneratedOutput(out RazorCodeDocument results); + } +} diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DocumentState.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DocumentState.cs new file mode 100644 index 0000000000..f53adf18b0 --- /dev/null +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DocumentState.cs @@ -0,0 +1,73 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.CodeAnalysis.Host; + +namespace Microsoft.CodeAnalysis.Razor.ProjectSystem +{ + internal class DocumentState + { + private readonly object _lock; + + private DocumentGeneratedOutputTracker _generatedOutput; + + public DocumentState(HostWorkspaceServices services, HostDocument hostDocument) + { + if (services == null) + { + throw new ArgumentNullException(nameof(services)); + } + + if (hostDocument == null) + { + throw new ArgumentNullException(nameof(hostDocument)); + } + + Services = services; + HostDocument = hostDocument; + Version = VersionStamp.Create(); + + _lock = new object(); + } + + public DocumentState(DocumentState previous, ProjectDifference difference) + { + if (previous == null) + { + throw new ArgumentNullException(nameof(previous)); + } + + Services = previous.Services; + HostDocument = previous.HostDocument; + Version = previous.Version.GetNewerVersion(); + + _generatedOutput = previous._generatedOutput?.ForkFor(this, difference); + } + + public HostDocument HostDocument { get; } + + public HostWorkspaceServices Services { get; } + + public VersionStamp Version { get; } + + public DocumentGeneratedOutputTracker GeneratedOutput + { + get + { + if (_generatedOutput == null) + { + lock (_lock) + { + if (_generatedOutput == null) + { + _generatedOutput = new DocumentGeneratedOutputTracker(null); + } + } + } + + return _generatedOutput; + } + } + } +} diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/EphemeralProjectSnapshot.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/EphemeralProjectSnapshot.cs new file mode 100644 index 0000000000..c05c4f6e32 --- /dev/null +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/EphemeralProjectSnapshot.cs @@ -0,0 +1,81 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.CodeAnalysis.Host; + +namespace Microsoft.CodeAnalysis.Razor.ProjectSystem +{ + internal class EphemeralProjectSnapshot : ProjectSnapshot + { + private static readonly Task> EmptyTagHelpers = Task.FromResult>(Array.Empty()); + + private readonly HostWorkspaceServices _services; + private readonly Lazy _projectEngine; + + public EphemeralProjectSnapshot(HostWorkspaceServices services, string filePath) + { + if (services == null) + { + throw new ArgumentNullException(nameof(services)); + } + + if (filePath == null) + { + throw new ArgumentNullException(nameof(filePath)); + } + + _services = services; + FilePath = filePath; + + _projectEngine = new Lazy(CreateProjectEngine); + } + + public override RazorConfiguration Configuration => FallbackRazorConfiguration.MVC_2_1; + + public override IEnumerable DocumentFilePaths => Array.Empty(); + + public override string FilePath { get; } + + public override bool IsInitialized => false; + + public override VersionStamp Version { get; } = VersionStamp.Default; + + public override Project WorkspaceProject => null; + + public override DocumentSnapshot GetDocument(string filePath) + { + if (filePath == null) + { + throw new ArgumentNullException(nameof(filePath)); + } + + return null; + } + + public override RazorProjectEngine GetProjectEngine() + { + return _projectEngine.Value; + } + + public override Task> GetTagHelpersAsync() + { + return EmptyTagHelpers; + } + + public override bool TryGetTagHelpers(out IReadOnlyList results) + { + results = EmptyTagHelpers.Result; + return true; + } + + private RazorProjectEngine CreateProjectEngine() + { + var factory = _services.GetRequiredService(); + return factory.Create(this); + } + } +} diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/HostDocument.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/HostDocument.cs new file mode 100644 index 0000000000..146943cab6 --- /dev/null +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/HostDocument.cs @@ -0,0 +1,61 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.Extensions.Internal; + +namespace Microsoft.CodeAnalysis.Razor.ProjectSystem +{ + internal class HostDocument : IEquatable + { + public HostDocument(string filePath, string targetPath) + { + if (filePath == null) + { + throw new ArgumentNullException(nameof(filePath)); + } + + if (targetPath == null) + { + throw new ArgumentNullException(nameof(targetPath)); + } + + FilePath = filePath; + TargetPath = targetPath; + } + + public string FilePath { get; } + + public string TargetPath { get; } + + public override bool Equals(object obj) + { + return base.Equals(obj as DocumentSnapshot); + } + + public bool Equals(DocumentSnapshot other) + { + if (ReferenceEquals(null, other)) + { + return false; + } + + return + FilePathComparer.Instance.Equals(FilePath, other.FilePath) && + FilePathComparer.Instance.Equals(TargetPath, other.TargetPath); + } + + public bool Equals(HostDocument other) + { + throw new NotImplementedException(); + } + + public override int GetHashCode() + { + var hash = new HashCodeCombiner(); + hash.Add(FilePath, FilePathComparer.Instance); + hash.Add(TargetPath, FilePathComparer.Instance); + return hash; + } + } +} diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectChangeEventArgs.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectChangeEventArgs.cs index eca6b56774..0c0faa3db8 100644 --- a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectChangeEventArgs.cs +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectChangeEventArgs.cs @@ -7,13 +7,13 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem { internal class ProjectChangeEventArgs : EventArgs { - public ProjectChangeEventArgs(ProjectSnapshot project, ProjectChangeKind kind) + public ProjectChangeEventArgs(string projectFilePath, ProjectChangeKind kind) { - Project = project; + ProjectFilePath = projectFilePath; Kind = kind; } - public ProjectSnapshot Project { get; } + public string ProjectFilePath { get; } public ProjectChangeKind Kind { get; } } diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectChangeKind.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectChangeKind.cs index c2ff3feacf..53b6b36529 100644 --- a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectChangeKind.cs +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectChangeKind.cs @@ -5,9 +5,10 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem { internal enum ProjectChangeKind { - Added, - Removed, - Changed, - TagHelpersChanged, + ProjectAdded, + ProjectRemoved, + ProjectChanged, + DocumentsChanged, + DocumentContentChanged, } } diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectDifference.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectDifference.cs new file mode 100644 index 0000000000..d450393f3c --- /dev/null +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectDifference.cs @@ -0,0 +1,18 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +namespace Microsoft.CodeAnalysis.Razor.ProjectSystem +{ + [Flags] + internal enum ProjectDifference + { + None = 0, + ConfigurationChanged = 1, + WorkspaceProjectAdded = 2, + WorkspaceProjectRemoved = 4, + WorkspaceProjectChanged = 8, + DocumentsChanged = 16, + } +} diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectEngineTracker.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectEngineTracker.cs new file mode 100644 index 0000000000..6e54daed0b --- /dev/null +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectEngineTracker.cs @@ -0,0 +1,66 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.CodeAnalysis.Host; + +namespace Microsoft.CodeAnalysis.Razor.ProjectSystem +{ + internal class ProjectEngineTracker + { + private const ProjectDifference Mask = ProjectDifference.ConfigurationChanged; + + private readonly object _lock = new object(); + + private readonly HostWorkspaceServices _services; + private RazorProjectEngine _projectEngine; + + public ProjectEngineTracker(ProjectState state) + { + if (state == null) + { + throw new ArgumentNullException(nameof(state)); + } + + _services = state.Services; + } + + public ProjectEngineTracker ForkFor(ProjectState state, ProjectDifference difference) + { + if (state == null) + { + throw new ArgumentNullException(nameof(state)); + } + + if ((difference & Mask) != 0) + { + return null; + } + + return this; + } + + public RazorProjectEngine GetProjectEngine(ProjectSnapshot snapshot) + { + if (snapshot == null) + { + throw new ArgumentNullException(nameof(snapshot)); + } + + if (_projectEngine == null) + { + lock (_lock) + { + if (_projectEngine == null) + { + var factory = _services.GetRequiredService(); + _projectEngine = factory.Create(snapshot); + } + } + } + + return _projectEngine; + } + } +} diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectExtensibilityAssembly.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectExtensibilityAssembly.cs deleted file mode 100644 index 43839f3664..0000000000 --- a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectExtensibilityAssembly.cs +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; - -namespace Microsoft.CodeAnalysis.Razor.ProjectSystem -{ - internal sealed class ProjectExtensibilityAssembly : IEquatable - { - public ProjectExtensibilityAssembly(AssemblyIdentity identity) - { - if (identity == null) - { - throw new ArgumentNullException(nameof(identity)); - } - - Identity = identity; - } - - public AssemblyIdentity Identity { get; } - - public bool Equals(ProjectExtensibilityAssembly other) - { - if (other == null) - { - return false; - } - - return Identity.Equals(other.Identity); - } - - public override int GetHashCode() - { - return Identity.GetHashCode(); - } - - public override bool Equals(object obj) - { - return base.Equals(obj as ProjectExtensibilityAssembly); - } - } -} diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSnapshot.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSnapshot.cs index 538b3cb9fa..1b6ce7abff 100644 --- a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSnapshot.cs +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSnapshot.cs @@ -1,8 +1,8 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using System; using System.Collections.Generic; +using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.Language; namespace Microsoft.CodeAnalysis.Razor.ProjectSystem @@ -11,16 +11,22 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem { public abstract RazorConfiguration Configuration { get; } + public abstract IEnumerable DocumentFilePaths { get; } + public abstract string FilePath { get; } public abstract bool IsInitialized { get; } - public abstract IReadOnlyList TagHelpers { get; } - public abstract VersionStamp Version { get; } public abstract Project WorkspaceProject { get; } - public abstract HostProject HostProject { get; } + public abstract RazorProjectEngine GetProjectEngine(); + + public abstract DocumentSnapshot GetDocument(string filePath); + + public abstract Task> GetTagHelpersAsync(); + + public abstract bool TryGetTagHelpers(out IReadOnlyList results); } } \ No newline at end of file diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSnapshotManager.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSnapshotManager.cs index b9e39e00b1..e491674399 100644 --- a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSnapshotManager.cs +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSnapshotManager.cs @@ -12,5 +12,9 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem public abstract event EventHandler Changed; public abstract IReadOnlyList Projects { get; } + + public abstract ProjectSnapshot GetLoadedProject(string filePath); + + public abstract ProjectSnapshot GetOrCreateProject(string filePath); } } diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSnapshotManagerBase.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSnapshotManagerBase.cs index 026b27956f..87306d1b5a 100644 --- a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSnapshotManagerBase.cs +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSnapshotManagerBase.cs @@ -9,7 +9,9 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem { public abstract Workspace Workspace { get; } - public abstract void ProjectUpdated(ProjectSnapshotUpdateContext update); + public abstract void DocumentAdded(HostProject hostProject, HostDocument hostDocument); + + public abstract void DocumentRemoved(HostProject hostProject, HostDocument hostDocument); public abstract void HostProjectAdded(HostProject hostProject); @@ -17,8 +19,6 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem public abstract void HostProjectRemoved(HostProject hostProject); - public abstract void HostProjectBuildComplete(HostProject hostProject); - public abstract void WorkspaceProjectAdded(Project workspaceProject); public abstract void WorkspaceProjectChanged(Project workspaceProject); diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSnapshotManagerExtensions.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSnapshotManagerExtensions.cs deleted file mode 100644 index d6299717ca..0000000000 --- a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSnapshotManagerExtensions.cs +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; - -namespace Microsoft.CodeAnalysis.Razor.ProjectSystem -{ - internal static class ProjectSnapshotManagerExtensions - { - public static ProjectSnapshot GetProjectWithFilePath(this ProjectSnapshotManager snapshotManager, string filePath) - { - var projects = snapshotManager.Projects; - for (var i = 0; i< projects.Count; i++) - { - var project = projects[i]; - if (FilePathComparer.Instance.Equals(filePath, project.FilePath)) - { - return project; - } - } - - return null; - } - } -} diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSnapshotUpdateContext.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSnapshotUpdateContext.cs deleted file mode 100644 index cddb3b08c1..0000000000 --- a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSnapshotUpdateContext.cs +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.Collections.Generic; -using Microsoft.AspNetCore.Razor.Language; - -namespace Microsoft.CodeAnalysis.Razor.ProjectSystem -{ - internal class ProjectSnapshotUpdateContext - { - public ProjectSnapshotUpdateContext(string filePath, HostProject hostProject, Project workspaceProject, VersionStamp version) - { - if (filePath == null) - { - throw new ArgumentNullException(nameof(filePath)); - } - - if (hostProject == null) - { - throw new ArgumentNullException(nameof(hostProject)); - } - - if (workspaceProject == null) - { - throw new ArgumentNullException(nameof(workspaceProject)); - } - - FilePath = filePath; - HostProject = hostProject; - WorkspaceProject = workspaceProject; - Version = version; - } - - public string FilePath { get; } - - public HostProject HostProject { get; } - - public Project WorkspaceProject { get; } - - public IReadOnlyList TagHelpers { get; set; } - - public VersionStamp Version { get; } - } -} diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSnapshotWorker.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSnapshotWorker.cs deleted file mode 100644 index 5c6288ee22..0000000000 --- a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSnapshotWorker.cs +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System.Threading; -using System.Threading.Tasks; -using Microsoft.CodeAnalysis.Host; - -namespace Microsoft.CodeAnalysis.Razor.ProjectSystem -{ - internal abstract class ProjectSnapshotWorker : ILanguageService - { - public abstract Task ProcessUpdateAsync(ProjectSnapshotUpdateContext update, CancellationToken cancellationToken = default(CancellationToken)); - } -} diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSnapshotWorkerQueue.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSnapshotWorkerQueue.cs deleted file mode 100644 index 69c0062f05..0000000000 --- a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSnapshotWorkerQueue.cs +++ /dev/null @@ -1,203 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; - -namespace Microsoft.CodeAnalysis.Razor.ProjectSystem -{ - internal class ProjectSnapshotWorkerQueue - { - private readonly ForegroundDispatcher _foregroundDispatcher; - private readonly DefaultProjectSnapshotManager _projectManager; - private readonly ProjectSnapshotWorker _projectWorker; - - private readonly Dictionary _projects; - private Timer _timer; - - public ProjectSnapshotWorkerQueue(ForegroundDispatcher foregroundDispatcher, DefaultProjectSnapshotManager projectManager, ProjectSnapshotWorker projectWorker) - { - if (foregroundDispatcher == null) - { - throw new ArgumentNullException(nameof(foregroundDispatcher)); - } - - if (projectManager == null) - { - throw new ArgumentNullException(nameof(projectManager)); - } - - if (projectWorker == null) - { - throw new ArgumentNullException(nameof(projectWorker)); - } - - _foregroundDispatcher = foregroundDispatcher; - _projectManager = projectManager; - _projectWorker = projectWorker; - - _projects = new Dictionary(FilePathComparer.Instance); - } - - public bool HasPendingNotifications - { - get - { - lock (_projects) - { - return _projects.Count > 0; - } - } - } - - // Used in unit tests to control the timer delay. - public TimeSpan Delay { get; set; } = TimeSpan.FromSeconds(2); - - public bool IsScheduledOrRunning => _timer != null; - - // Used in unit tests to ensure we can control when background work starts. - public ManualResetEventSlim BlockBackgroundWorkStart { get; set; } - - // Used in unit tests to ensure we can know when background work finishes. - public ManualResetEventSlim NotifyBackgroundWorkFinish { get; set; } - - // Used in unit tests to ensure we can be notified when all completes. - public ManualResetEventSlim NotifyForegroundWorkFinish { get; set; } - - private void OnStartingBackgroundWork() - { - if (BlockBackgroundWorkStart != null) - { - BlockBackgroundWorkStart.Wait(); - BlockBackgroundWorkStart.Reset(); - } - } - - private void OnFinishingBackgroundWork() - { - if (NotifyBackgroundWorkFinish != null) - { - NotifyBackgroundWorkFinish.Set(); - } - } - - private void OnFinishingForegroundWork() - { - if (NotifyForegroundWorkFinish != null) - { - NotifyForegroundWorkFinish.Set(); - } - } - - public void Enqueue(ProjectSnapshotUpdateContext context) - { - if (context == null) - { - throw new ArgumentNullException(nameof(context)); - } - - _foregroundDispatcher.AssertForegroundThread(); - - lock (_projects) - { - // We only want to store the last 'seen' version of any given project. That way when we pick one to process - // it's always the best version to use. - _projects[context.FilePath] = context; - - StartWorker(); - } - } - - protected virtual void StartWorker() - { - // Access to the timer is protected by the lock in Enqueue and in Timer_Tick - if (_timer == null) - { - // Timer will fire after a fixed delay, but only once. - _timer = new Timer(Timer_Tick, null, Delay, Timeout.InfiniteTimeSpan); - } - } - - private async void Timer_Tick(object state) // Yeah I know. - { - try - { - _foregroundDispatcher.AssertBackgroundThread(); - - // Timer is stopped. - _timer.Change(Timeout.Infinite, Timeout.Infinite); - - OnStartingBackgroundWork(); - - ProjectSnapshotUpdateContext[] work; - lock (_projects) - { - work = _projects.Values.ToArray(); - _projects.Clear(); - } - - var updates = new(ProjectSnapshotUpdateContext context, Exception exception)[work.Length]; - for (var i = 0; i < work.Length; i++) - { - try - { - updates[i] = (work[i], null); - await _projectWorker.ProcessUpdateAsync(updates[i].context); - } - catch (Exception projectException) - { - updates[i] = (updates[i].context, projectException); - } - } - - OnFinishingBackgroundWork(); - - // We need to get back to the UI thread to update the project system. - await Task.Factory.StartNew(PersistUpdates, updates, CancellationToken.None, TaskCreationOptions.None, _foregroundDispatcher.ForegroundScheduler); - - lock (_projects) - { - // Resetting the timer allows another batch of work to start. - _timer.Dispose(); - _timer = null; - - // If more work came in while we were running start the worker again. - if (_projects.Count > 0) - { - StartWorker(); - } - } - - OnFinishingForegroundWork(); - } - catch (Exception ex) - { - // This is something totally unexpected, let's just send it over to the workspace. - await Task.Factory.StartNew(() => _projectManager.ReportError(ex), CancellationToken.None, TaskCreationOptions.None, _foregroundDispatcher.ForegroundScheduler); - } - } - - private void PersistUpdates(object state) - { - _foregroundDispatcher.AssertForegroundThread(); - - var updates = ((ProjectSnapshotUpdateContext context, Exception exception)[])state; - - for (var i = 0; i < updates.Length; i++) - { - var update = updates[i]; - if (update.exception == null) - { - _projectManager.ProjectUpdated(update.context); - } - else - { - _projectManager.ReportError(update.exception, update.context?.WorkspaceProject); - } - } - } - } -} diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectState.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectState.cs new file mode 100644 index 0000000000..05ecf2be49 --- /dev/null +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectState.cs @@ -0,0 +1,239 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using Microsoft.CodeAnalysis.Host; + +namespace Microsoft.CodeAnalysis.Razor.ProjectSystem +{ + // Internal tracker for DefaultProjectSnapshot + internal class ProjectState + { + private static readonly IReadOnlyDictionary EmptyDocuments = new Dictionary(); + + private readonly object _lock; + + private ProjectEngineTracker _projectEngine; + private ProjectTagHelperTracker _tagHelpers; + + public ProjectState( + HostWorkspaceServices services, + HostProject hostProject, + Project workspaceProject) + { + if (services == null) + { + throw new ArgumentNullException(nameof(services)); + } + + if (hostProject == null) + { + throw new ArgumentNullException(nameof(hostProject)); + } + + Services = services; + HostProject = hostProject; + WorkspaceProject = workspaceProject; + Documents = EmptyDocuments; + Version = VersionStamp.Create(); + + _lock = new object(); + } + + public ProjectState( + ProjectState older, + ProjectDifference difference, + HostProject hostProject, + Project workspaceProject, + IReadOnlyDictionary documents) + { + if (older == null) + { + throw new ArgumentNullException(nameof(older)); + } + + if (hostProject == null) + { + throw new ArgumentNullException(nameof(hostProject)); + } + + if (documents == null) + { + throw new ArgumentNullException(nameof(documents)); + } + + Services = older.Services; + Version = older.Version.GetNewerVersion(); + + HostProject = hostProject; + WorkspaceProject = workspaceProject; + Documents = documents; + + _lock = new object(); + + _projectEngine = older._projectEngine?.ForkFor(this, difference); + _tagHelpers = older._tagHelpers?.ForkFor(this, difference); + } + + public IReadOnlyDictionary Documents { get; } + + public HostProject HostProject { get; } + + public HostWorkspaceServices Services { get; } + + public Project WorkspaceProject { get; } + + public VersionStamp Version { get; } + + // Computed State + public ProjectEngineTracker ProjectEngine + { + get + { + if (_projectEngine == null) + { + lock (_lock) + { + if (_projectEngine == null) + { + _projectEngine = new ProjectEngineTracker(this); + } + } + } + + return _projectEngine; + } + } + + // Computed State + public ProjectTagHelperTracker TagHelpers + { + get + { + if (_tagHelpers == null) + { + lock (_lock) + { + if (_tagHelpers == null) + { + _tagHelpers = new ProjectTagHelperTracker(this); + } + } + } + + return _tagHelpers; + } + } + + public ProjectState AddHostDocument(HostDocument hostDocument) + { + if (hostDocument == null) + { + throw new ArgumentNullException(nameof(hostDocument)); + } + + // Ignore attempts to 'add' a document with different data, we only + // care about one, so it might as well be the one we have. + if (Documents.ContainsKey(hostDocument.FilePath)) + { + return this; + } + + var documents = new Dictionary(FilePathComparer.Instance); + foreach (var kvp in Documents) + { + documents.Add(kvp.Key, kvp.Value); + } + + documents.Add(hostDocument.FilePath, new DocumentState(Services, hostDocument)); + + var difference = ProjectDifference.DocumentsChanged; + var state = new ProjectState(this, difference, HostProject, WorkspaceProject, documents); + return state; + } + + public ProjectState RemoveHostDocument(HostDocument hostDocument) + { + if (hostDocument == null) + { + throw new ArgumentNullException(nameof(hostDocument)); + } + + if (!Documents.ContainsKey(hostDocument.FilePath)) + { + return this; + } + + var documents = new Dictionary(FilePathComparer.Instance); + foreach (var kvp in Documents) + { + documents.Add(kvp.Key, kvp.Value); + } + + documents.Remove(hostDocument.FilePath); + + var difference = ProjectDifference.DocumentsChanged; + var state = new ProjectState(this, difference, HostProject, WorkspaceProject, documents); + return state; + } + + public ProjectState WithHostProject(HostProject hostProject) + { + if (hostProject == null) + { + throw new ArgumentNullException(nameof(hostProject)); + } + + if (HostProject.Configuration.Equals(hostProject.Configuration)) + { + return this; + } + + var difference = ProjectDifference.ConfigurationChanged; + var documents = new Dictionary(FilePathComparer.Instance); + foreach (var kvp in Documents) + { + documents.Add(kvp.Key, new DocumentState(kvp.Value, difference)); + } + + var state = new ProjectState(this, difference, hostProject, WorkspaceProject, documents); + return state; + } + + public ProjectState WithWorkspaceProject(Project workspaceProject) + { + var difference = ProjectDifference.None; + if (WorkspaceProject == null && workspaceProject != null) + { + difference |= ProjectDifference.WorkspaceProjectAdded; + } + else if (WorkspaceProject != null && workspaceProject == null) + { + difference |= ProjectDifference.WorkspaceProjectRemoved; + } + else if ( + WorkspaceProject?.Id != workspaceProject?.Id || + WorkspaceProject?.Version != workspaceProject?.Version) + { + // For now this is very naive. We will want to consider changing + // our logic here to be more robust. + difference |= ProjectDifference.WorkspaceProjectChanged; + } + + if (difference == ProjectDifference.None) + { + return this; + } + + var documents = new Dictionary(FilePathComparer.Instance); + foreach (var kvp in Documents) + { + documents.Add(kvp.Key, new DocumentState(kvp.Value, difference)); + } + + var state = new ProjectState(this, difference, HostProject, workspaceProject, documents); + return state; + } + } +} diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectTagHelperTracker.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectTagHelperTracker.cs new file mode 100644 index 0000000000..0c3be4ddec --- /dev/null +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectTagHelperTracker.cs @@ -0,0 +1,79 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.CodeAnalysis.Host; + +namespace Microsoft.CodeAnalysis.Razor.ProjectSystem +{ + internal class ProjectTagHelperTracker + { + private const ProjectDifference Mask = + ProjectDifference.ConfigurationChanged | + ProjectDifference.WorkspaceProjectAdded | + ProjectDifference.WorkspaceProjectChanged | + ProjectDifference.WorkspaceProjectRemoved; + + private readonly object _lock = new object(); + private readonly HostWorkspaceServices _services; + + private Task> _task; + + public ProjectTagHelperTracker(ProjectState state) + { + if (state == null) + { + throw new ArgumentNullException(nameof(state)); + } + + _services = state.Services; + } + + public bool IsResultAvailable => _task?.IsCompleted == true; + + public ProjectTagHelperTracker ForkFor(ProjectState state, ProjectDifference difference) + { + if (state == null) + { + throw new ArgumentNullException(nameof(state)); + } + + if ((difference & Mask) != 0) + { + return null; + } + + return this; + } + + public Task> GetTagHelperInitializationTask(ProjectSnapshot snapshot) + { + if (snapshot == null) + { + throw new ArgumentNullException(nameof(snapshot)); + } + + if (_task == null) + { + lock (_lock) + { + if (_task == null) + { + _task = GetTagHelperInitializationTaskCore(snapshot); + } + } + } + + return _task; + } + + private async Task> GetTagHelperInitializationTaskCore(ProjectSnapshot snapshot) + { + var resolver = _services.GetLanguageServices(RazorLanguage.Name).GetRequiredService(); + return (await resolver.GetTagHelpersAsync(snapshot)).Descriptors; + } + } +} diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/RazorProjectEngineFactoryService.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/RazorProjectEngineFactoryService.cs deleted file mode 100644 index 4defb21947..0000000000 --- a/src/Microsoft.CodeAnalysis.Razor.Workspaces/RazorProjectEngineFactoryService.cs +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using Microsoft.AspNetCore.Razor.Language; -using Microsoft.CodeAnalysis.Host; -using Microsoft.CodeAnalysis.Razor.ProjectSystem; - -namespace Microsoft.CodeAnalysis.Razor -{ - internal abstract class RazorProjectEngineFactoryService : ILanguageService - { - public abstract IProjectEngineFactory FindFactory(ProjectSnapshot project); - - public abstract IProjectEngineFactory FindSerializableFactory(ProjectSnapshot project); - - public abstract RazorProjectEngine Create(ProjectSnapshot project, Action configure); - - public abstract RazorProjectEngine Create(ProjectSnapshot project, RazorProjectFileSystem fileSystem, Action configure); - - public abstract RazorProjectEngine Create(string directoryPath, Action configure); - } -} \ No newline at end of file diff --git a/src/Microsoft.CodeAnalysis.Remote.Razor/GeneratedDocument.cs b/src/Microsoft.CodeAnalysis.Remote.Razor/GeneratedDocument.cs deleted file mode 100644 index 8299fce595..0000000000 --- a/src/Microsoft.CodeAnalysis.Remote.Razor/GeneratedDocument.cs +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -namespace Microsoft.CodeAnalysis.Remote.Razor -{ - internal class GeneratedDocument - { - public string Text { get; set; } - } -} diff --git a/src/Microsoft.CodeAnalysis.Remote.Razor/RazorLanguageService.cs b/src/Microsoft.CodeAnalysis.Remote.Razor/RazorLanguageService.cs index ecde04c752..16ababe0db 100644 --- a/src/Microsoft.CodeAnalysis.Remote.Razor/RazorLanguageService.cs +++ b/src/Microsoft.CodeAnalysis.Remote.Razor/RazorLanguageService.cs @@ -2,13 +2,9 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using System.Collections.Generic; using System.IO; -using System.Linq; -using System.Text; using System.Threading; using System.Threading.Tasks; -using Microsoft.AspNetCore.Razor.Language; using Microsoft.CodeAnalysis.Razor; using Microsoft.CodeAnalysis.Razor.ProjectSystem; @@ -27,42 +23,5 @@ namespace Microsoft.CodeAnalysis.Remote.Razor return await RazorServices.TagHelperResolver.GetTagHelpersAsync(project, factoryTypeName, cancellationToken); } - - public Task> GetDirectivesAsync(Guid projectIdBytes, string projectDebugName, CancellationToken cancellationToken = default(CancellationToken)) - { - var projectId = ProjectId.CreateFromSerialized(projectIdBytes, projectDebugName); - - var projectEngine = RazorProjectEngine.Create(); - var directives = projectEngine.EngineFeatures.OfType().FirstOrDefault()?.Directives; - return Task.FromResult(directives ?? Enumerable.Empty()); - } - - public Task GenerateDocumentAsync(Guid projectIdBytes, string projectDebugName, string filePath, string text, CancellationToken cancellationToken = default(CancellationToken)) - { - var projectId = ProjectId.CreateFromSerialized(projectIdBytes, projectDebugName); - - var projectEngine = RazorProjectEngine.Create(); - - RazorSourceDocument source; - using (var stream = new MemoryStream()) - { - var bytes = Encoding.UTF8.GetBytes(text); - stream.Write(bytes, 0, bytes.Length); - - stream.Seek(0L, SeekOrigin.Begin); - source = RazorSourceDocument.ReadFrom(stream, filePath, Encoding.UTF8); - } - - var code = RazorCodeDocument.Create(source); - projectEngine.Engine.Process(code); - - var csharp = code.GetCSharpDocument(); - if (csharp == null) - { - throw new InvalidOperationException(); - } - - return Task.FromResult(new GeneratedDocument() { Text = csharp.GeneratedCode, }); - } } } diff --git a/src/Microsoft.CodeAnalysis.Remote.Razor/RazorServiceBase.cs b/src/Microsoft.CodeAnalysis.Remote.Razor/RazorServiceBase.cs index 8fdbdd81da..91ac18ae16 100644 --- a/src/Microsoft.CodeAnalysis.Remote.Razor/RazorServiceBase.cs +++ b/src/Microsoft.CodeAnalysis.Remote.Razor/RazorServiceBase.cs @@ -48,9 +48,7 @@ namespace Microsoft.CodeAnalysis.Remote.Razor { FilePath = filePath; Configuration = configuration; - HostProject = new HostProject(filePath, configuration); WorkspaceProject = workspaceProject; - TagHelpers = Array.Empty(); IsInitialized = true; Version = VersionStamp.Default; @@ -58,6 +56,8 @@ namespace Microsoft.CodeAnalysis.Remote.Razor public override RazorConfiguration Configuration { get; } + public override IEnumerable DocumentFilePaths => Array.Empty(); + public override string FilePath { get; } public override bool IsInitialized { get; } @@ -66,9 +66,30 @@ namespace Microsoft.CodeAnalysis.Remote.Razor public override Project WorkspaceProject { get; } - public override HostProject HostProject { get; } + public override DocumentSnapshot GetDocument(string filePath) + { + if (filePath == null) + { + throw new ArgumentNullException(nameof(filePath)); + } - public override IReadOnlyList TagHelpers { get; } + return null; + } + + public override RazorProjectEngine GetProjectEngine() + { + throw new NotImplementedException(); + } + + public override Task> GetTagHelpersAsync() + { + throw new NotImplementedException(); + } + + public override bool TryGetTagHelpers(out IReadOnlyList results) + { + throw new NotImplementedException(); + } } } } diff --git a/src/Microsoft.NET.Sdk.Razor/build/netstandard2.0/Rules/RazorGenerateWithTargetPath.xaml b/src/Microsoft.NET.Sdk.Razor/build/netstandard2.0/Rules/RazorGenerateWithTargetPath.xaml new file mode 100644 index 0000000000..bab10bf249 --- /dev/null +++ b/src/Microsoft.NET.Sdk.Razor/build/netstandard2.0/Rules/RazorGenerateWithTargetPath.xaml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Microsoft.VisualStudio.Editor.Razor/DefaultImportDocumentManager.cs b/src/Microsoft.VisualStudio.Editor.Razor/DefaultImportDocumentManager.cs index 2e8c5a450a..68985bc9c6 100644 --- a/src/Microsoft.VisualStudio.Editor.Razor/DefaultImportDocumentManager.cs +++ b/src/Microsoft.VisualStudio.Editor.Razor/DefaultImportDocumentManager.cs @@ -16,7 +16,6 @@ namespace Microsoft.VisualStudio.Editor.Razor private readonly FileChangeTrackerFactory _fileChangeTrackerFactory; private readonly ForegroundDispatcher _foregroundDispatcher; private readonly ErrorReporter _errorReporter; - private readonly RazorProjectEngineFactoryService _projectEngineFactoryService; private readonly Dictionary _importTrackerCache; public override event EventHandler Changed; @@ -24,8 +23,7 @@ namespace Microsoft.VisualStudio.Editor.Razor public DefaultImportDocumentManager( ForegroundDispatcher foregroundDispatcher, ErrorReporter errorReporter, - FileChangeTrackerFactory fileChangeTrackerFactory, - RazorProjectEngineFactoryService projectEngineFactoryService) + FileChangeTrackerFactory fileChangeTrackerFactory) { if (foregroundDispatcher == null) { @@ -42,15 +40,9 @@ namespace Microsoft.VisualStudio.Editor.Razor throw new ArgumentNullException(nameof(fileChangeTrackerFactory)); } - if (projectEngineFactoryService == null) - { - throw new ArgumentNullException(nameof(projectEngineFactoryService)); - } - _foregroundDispatcher = foregroundDispatcher; _errorReporter = errorReporter; _fileChangeTrackerFactory = fileChangeTrackerFactory; - _projectEngineFactoryService = projectEngineFactoryService; _importTrackerCache = new Dictionary(StringComparer.OrdinalIgnoreCase); } @@ -115,8 +107,7 @@ namespace Microsoft.VisualStudio.Editor.Razor private IEnumerable GetImportItems(VisualStudioDocumentTracker tracker) { - var projectDirectory = Path.GetDirectoryName(tracker.ProjectPath); - var projectEngine = _projectEngineFactoryService.Create(projectDirectory, _ => { }); + var projectEngine = tracker.ProjectSnapshot.GetProjectEngine(); var trackerItem = projectEngine.FileSystem.GetItem(tracker.FilePath); var importFeature = projectEngine.ProjectFeatures.OfType().FirstOrDefault(); diff --git a/src/Microsoft.VisualStudio.Editor.Razor/DefaultImportDocumentManagerFactory.cs b/src/Microsoft.VisualStudio.Editor.Razor/DefaultImportDocumentManagerFactory.cs index 47efa8b96c..c44417d69f 100644 --- a/src/Microsoft.VisualStudio.Editor.Razor/DefaultImportDocumentManagerFactory.cs +++ b/src/Microsoft.VisualStudio.Editor.Razor/DefaultImportDocumentManagerFactory.cs @@ -35,13 +35,8 @@ namespace Microsoft.VisualStudio.Editor.Razor var errorReporter = languageServices.WorkspaceServices.GetRequiredService(); var fileChangeTrackerFactory = languageServices.GetRequiredService(); - var projectEngineFactoryService = languageServices.GetRequiredService(); - return new DefaultImportDocumentManager( - _foregroundDispatcher, - errorReporter, - fileChangeTrackerFactory, - projectEngineFactoryService); + return new DefaultImportDocumentManager(_foregroundDispatcher, errorReporter, fileChangeTrackerFactory); } } } diff --git a/src/Microsoft.VisualStudio.Editor.Razor/DefaultProjectEngineFactoryService.cs b/src/Microsoft.VisualStudio.Editor.Razor/DefaultProjectEngineFactoryService.cs deleted file mode 100644 index d70f82619e..0000000000 --- a/src/Microsoft.VisualStudio.Editor.Razor/DefaultProjectEngineFactoryService.cs +++ /dev/null @@ -1,196 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.IO; -using Microsoft.AspNetCore.Razor.Language; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Razor; -using Microsoft.CodeAnalysis.Razor.ProjectSystem; - -namespace Microsoft.VisualStudio.Editor.Razor -{ - internal class DefaultProjectEngineFactoryService : RazorProjectEngineFactoryService - { - private readonly static RazorConfiguration DefaultConfiguration = FallbackRazorConfiguration.MVC_2_1; - - private readonly Workspace _workspace; - private readonly IFallbackProjectEngineFactory _defaultFactory; - private readonly Lazy[] _customFactories; - private ProjectSnapshotManager _projectManager; - - public DefaultProjectEngineFactoryService( - Workspace workspace, - IFallbackProjectEngineFactory defaultFactory, - Lazy[] customFactories) - { - if (workspace == null) - { - throw new ArgumentNullException(nameof(workspace)); - } - - if (defaultFactory == null) - { - throw new ArgumentNullException(nameof(defaultFactory)); - } - - if (customFactories == null) - { - throw new ArgumentNullException(nameof(customFactories)); - } - - _workspace = workspace; - _defaultFactory = defaultFactory; - _customFactories = customFactories; - } - - // Internal for testing - internal DefaultProjectEngineFactoryService( - ProjectSnapshotManager projectManager, - IFallbackProjectEngineFactory defaultFactory, - Lazy[] customFactories) - { - if (projectManager == null) - { - throw new ArgumentNullException(nameof(projectManager)); - } - - if (defaultFactory == null) - { - throw new ArgumentNullException(nameof(defaultFactory)); - } - - if (customFactories == null) - { - throw new ArgumentNullException(nameof(customFactories)); - } - - _projectManager = projectManager; - _defaultFactory = defaultFactory; - _customFactories = customFactories; - } - - public override IProjectEngineFactory FindFactory(ProjectSnapshot project) - { - if (project == null) - { - throw new ArgumentNullException(nameof(project)); - } - - return SelectFactory(project.Configuration ?? DefaultConfiguration, requireSerializable: false); - } - - public override IProjectEngineFactory FindSerializableFactory(ProjectSnapshot project) - { - if (project == null) - { - throw new ArgumentNullException(nameof(project)); - } - - return SelectFactory(project.Configuration ?? DefaultConfiguration, requireSerializable: true); - } - - public override RazorProjectEngine Create(ProjectSnapshot project, Action configure) - { - if (project == null) - { - throw new ArgumentNullException(nameof(project)); - } - - return CreateCore(project, RazorProjectFileSystem.Create(Path.GetDirectoryName(project.FilePath)), configure); - } - - public override RazorProjectEngine Create(string directoryPath, Action configure) - { - if (directoryPath == null) - { - throw new ArgumentNullException(nameof(directoryPath)); - } - - var project = FindProjectByDirectory(directoryPath); - return CreateCore(project, RazorProjectFileSystem.Create(directoryPath), configure); - } - - public override RazorProjectEngine Create(ProjectSnapshot project, RazorProjectFileSystem fileSystem, Action configure) - { - if (project == null) - { - throw new ArgumentNullException(nameof(project)); - } - - if (fileSystem == null) - { - throw new ArgumentNullException(nameof(fileSystem)); - } - - return CreateCore(project, fileSystem, configure); - } - - private RazorProjectEngine CreateCore(ProjectSnapshot project, RazorProjectFileSystem fileSystem, Action configure) - { - // When we're running in the editor, the editor provides a configure delegate that will include - // the editor settings and tag helpers. - // - // This service is only used in process in Visual Studio, and any other callers should provide these - // things also. - configure = configure ?? ((b) => { }); - - // The default configuration currently matches the newest MVC configuration. - // - // We typically want this because the language adds features over time - we don't want to a bunch of errors - // to show up when a document is first opened, and then go away when the configuration loads, we'd prefer the opposite. - var configuration = project?.Configuration ?? DefaultConfiguration; - - // If there's no factory to handle the configuration then fall back to a very basic configuration. - // - // This will stop a crash from happening in this case (misconfigured project), but will still make - // it obvious to the user that something is wrong. - var factory = SelectFactory(configuration) ?? _defaultFactory; - return factory.Create(configuration, fileSystem, configure); - } - - private IProjectEngineFactory SelectFactory(RazorConfiguration configuration, bool requireSerializable = false) - { - for (var i = 0; i < _customFactories.Length; i++) - { - var factory = _customFactories[i]; - if (string.Equals(configuration.ConfigurationName, factory.Metadata.ConfigurationName)) - { - return requireSerializable && !factory.Metadata.SupportsSerialization ? null : factory.Value; - } - } - - return null; - } - - private ProjectSnapshot FindProjectByDirectory(string directory) - { - directory = NormalizeDirectoryPath(directory); - - if (_projectManager == null) - { - _projectManager = _workspace.Services.GetLanguageServices(RazorLanguage.Name).GetRequiredService(); - } - - var projects = _projectManager.Projects; - for (var i = 0; i < projects.Count; i++) - { - var project = projects[i]; - if (project.FilePath != null) - { - if (string.Equals(directory, NormalizeDirectoryPath(Path.GetDirectoryName(project.FilePath)), StringComparison.OrdinalIgnoreCase)) - { - return project; - } - } - } - - return null; - } - - private string NormalizeDirectoryPath(string path) - { - return path.Replace('\\', '/').TrimEnd('/'); - } - } -} diff --git a/src/Microsoft.VisualStudio.Editor.Razor/DefaultProjectEngineFactoryServiceFactory.cs b/src/Microsoft.VisualStudio.Editor.Razor/DefaultProjectEngineFactoryServiceFactory.cs deleted file mode 100644 index 7f22134479..0000000000 --- a/src/Microsoft.VisualStudio.Editor.Razor/DefaultProjectEngineFactoryServiceFactory.cs +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.Collections.Generic; -using System.Composition; -using System.Linq; -using Microsoft.CodeAnalysis.Host; -using Microsoft.CodeAnalysis.Host.Mef; -using Microsoft.CodeAnalysis.Razor; -using Microsoft.CodeAnalysis.Razor.ProjectSystem; - -namespace Microsoft.VisualStudio.Editor.Razor -{ - [ExportLanguageServiceFactory(typeof(RazorProjectEngineFactoryService), RazorLanguage.Name, ServiceLayer.Default)] - internal class DefaultProjectEngineFactoryServiceFactory : ILanguageServiceFactory - { - private readonly Lazy[] _customFactories; - private readonly IFallbackProjectEngineFactory _fallbackFactory; - - [ImportingConstructor] - public DefaultProjectEngineFactoryServiceFactory( - IFallbackProjectEngineFactory fallbackFactory, - [ImportMany] IEnumerable> customFactories) - { - if (fallbackFactory == null) - { - throw new ArgumentNullException(nameof(fallbackFactory)); - } - - if (customFactories == null) - { - throw new ArgumentNullException(nameof(customFactories)); - } - - _fallbackFactory = fallbackFactory; - _customFactories = customFactories.ToArray(); - } - - public ILanguageService CreateLanguageService(HostLanguageServices languageServices) - { - return new DefaultProjectEngineFactoryService( - languageServices.WorkspaceServices.Workspace, - _fallbackFactory, - _customFactories); - } - } -} \ No newline at end of file diff --git a/src/Microsoft.VisualStudio.Editor.Razor/DefaultTagHelperResolver.cs b/src/Microsoft.VisualStudio.Editor.Razor/DefaultTagHelperResolver.cs index 47453f6f80..400a0ce2b8 100644 --- a/src/Microsoft.VisualStudio.Editor.Razor/DefaultTagHelperResolver.cs +++ b/src/Microsoft.VisualStudio.Editor.Razor/DefaultTagHelperResolver.cs @@ -4,7 +4,6 @@ using System; using System.Threading; using System.Threading.Tasks; -using Microsoft.AspNetCore.Razor.Language; using Microsoft.CodeAnalysis.Razor; using Microsoft.CodeAnalysis.Razor.ProjectSystem; @@ -12,18 +11,6 @@ namespace Microsoft.VisualStudio.Editor.Razor { internal class DefaultTagHelperResolver : TagHelperResolver { - private readonly RazorProjectEngineFactoryService _engineFactory; - - public DefaultTagHelperResolver(RazorProjectEngineFactoryService engineFactory) - { - if (engineFactory == null) - { - throw new ArgumentNullException(nameof(engineFactory)); - } - - _engineFactory = engineFactory; - } - public override Task GetTagHelpersAsync(ProjectSnapshot project, CancellationToken cancellationToken = default) { if (project == null) @@ -35,9 +22,8 @@ namespace Microsoft.VisualStudio.Editor.Razor { return Task.FromResult(TagHelperResolutionResult.Empty); } - - var engine = _engineFactory.Create(project, RazorProjectFileSystem.Empty, b => { }); - return GetTagHelpersAsync(project, engine); + + return GetTagHelpersAsync(project, project.GetProjectEngine()); } } } diff --git a/src/Microsoft.VisualStudio.Editor.Razor/DefaultTagHelperResolverFactory.cs b/src/Microsoft.VisualStudio.Editor.Razor/DefaultTagHelperResolverFactory.cs index 1a238ad473..50e2e96179 100644 --- a/src/Microsoft.VisualStudio.Editor.Razor/DefaultTagHelperResolverFactory.cs +++ b/src/Microsoft.VisualStudio.Editor.Razor/DefaultTagHelperResolverFactory.cs @@ -14,7 +14,7 @@ namespace Microsoft.VisualStudio.Editor.Razor { public ILanguageService CreateLanguageService(HostLanguageServices languageServices) { - return new DefaultTagHelperResolver(languageServices.GetRequiredService()); + return new DefaultTagHelperResolver(); } } } \ No newline at end of file diff --git a/src/Microsoft.VisualStudio.Editor.Razor/DefaultVisualStudioDocumentTracker.cs b/src/Microsoft.VisualStudio.Editor.Razor/DefaultVisualStudioDocumentTracker.cs index c6e6937b18..bc31eba667 100644 --- a/src/Microsoft.VisualStudio.Editor.Razor/DefaultVisualStudioDocumentTracker.cs +++ b/src/Microsoft.VisualStudio.Editor.Razor/DefaultVisualStudioDocumentTracker.cs @@ -3,6 +3,9 @@ using System; using System.Collections.Generic; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.Language; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Razor; @@ -25,7 +28,13 @@ namespace Microsoft.VisualStudio.Editor.Razor private readonly List _textViews; private readonly Workspace _workspace; private bool _isSupportedProject; - private ProjectSnapshot _project; + private ProjectSnapshot _projectSnapshot; + + // Only allow a single tag helper computation task at a time. + private (ProjectSnapshot project, Task task) _computingTagHelpers; + + // Stores the result from the last time we computed tag helpers. + private IReadOnlyList _tagHelpers; public override event EventHandler ContextChanged; @@ -89,17 +98,23 @@ namespace Microsoft.VisualStudio.Editor.Razor _workspace = workspace; // For now we assume that the workspace is the always default VS workspace. _textViews = new List(); + _tagHelpers = Array.Empty(); } - public override RazorConfiguration Configuration => _project?.Configuration; + public override RazorConfiguration Configuration => _projectSnapshot?.Configuration; public override EditorSettings EditorSettings => _workspaceEditorSettings.Current; - public override IReadOnlyList TagHelpers => _project?.TagHelpers ?? Array.Empty(); + public override IReadOnlyList TagHelpers => _tagHelpers; public override bool IsSupportedProject => _isSupportedProject; - public override Project Project => _workspace.CurrentSolution.GetProject(_project.WorkspaceProject.Id); + public override Project Project => + _projectSnapshot.WorkspaceProject == null ? + null : + _workspace.CurrentSolution.GetProject(_projectSnapshot.WorkspaceProject.Id); + + internal override ProjectSnapshot ProjectSnapshot => _projectSnapshot; public override ITextBuffer TextBuffer => _textBuffer; @@ -111,6 +126,8 @@ namespace Microsoft.VisualStudio.Editor.Razor public override string ProjectPath => _projectPath; + public Task PendingTagHelperTask => _computingTagHelpers.task ?? Task.CompletedTask; + internal void AddTextView(ITextView textView) { if (textView == null) @@ -118,6 +135,8 @@ namespace Microsoft.VisualStudio.Editor.Razor throw new ArgumentNullException(nameof(textView)); } + _foregroundDispatcher.AssertForegroundThread(); + if (!_textViews.Contains(textView)) { _textViews.Add(textView); @@ -131,6 +150,8 @@ namespace Microsoft.VisualStudio.Editor.Razor throw new ArgumentNullException(nameof(textView)); } + _foregroundDispatcher.AssertForegroundThread(); + if (_textViews.Contains(textView)) { _textViews.Remove(textView); @@ -139,6 +160,8 @@ namespace Microsoft.VisualStudio.Editor.Razor public override ITextView GetFocusedTextView() { + _foregroundDispatcher.AssertForegroundThread(); + for (var i = 0; i < TextViews.Count; i++) { if (TextViews[i].HasAggregateFocus) @@ -152,20 +175,23 @@ namespace Microsoft.VisualStudio.Editor.Razor public void Subscribe() { + _foregroundDispatcher.AssertForegroundThread(); + + _projectSnapshot = _projectManager.GetOrCreateProject(_projectPath); + _isSupportedProject = true; + + _projectManager.Changed += ProjectManager_Changed; + _workspaceEditorSettings.Changed += EditorSettingsManager_Changed; + _importDocumentManager.Changed += Import_Changed; _importDocumentManager.OnSubscribed(this); - _workspaceEditorSettings.Changed += EditorSettingsManager_Changed; - _projectManager.Changed += ProjectManager_Changed; - _importDocumentManager.Changed += Import_Changed; - - _isSupportedProject = true; - _project = _projectManager.GetProjectWithFilePath(_projectPath); - - OnContextChanged(_project, ContextChangeKind.ProjectChanged); + OnContextChanged(ContextChangeKind.ProjectChanged); } public void Unsubscribe() { + _foregroundDispatcher.AssertForegroundThread(); + _importDocumentManager.OnUnsubscribed(this); _projectManager.Changed -= ProjectManager_Changed; @@ -174,37 +200,112 @@ namespace Microsoft.VisualStudio.Editor.Razor // Detached from project. _isSupportedProject = false; - _project = null; - - OnContextChanged(project: null, kind: ContextChangeKind.ProjectChanged); + _projectSnapshot = null; + OnContextChanged(kind: ContextChangeKind.ProjectChanged); } - private void OnContextChanged(ProjectSnapshot project, ContextChangeKind kind) + private void StartComputingTagHelpers() { _foregroundDispatcher.AssertForegroundThread(); - _project = project; + Debug.Assert(_projectSnapshot != null); + Debug.Assert(_computingTagHelpers.project == null && _computingTagHelpers.task == null); + + if (_projectSnapshot.TryGetTagHelpers(out var results)) + { + _tagHelpers = results; + OnContextChanged(ContextChangeKind.TagHelpersChanged); + return; + } + + // if we get here then we know the tag helpers aren't available, so force async for ease of testing + var task = _projectSnapshot + .GetTagHelpersAsync() + .ContinueWith(TagHelpersUpdated, CancellationToken.None, TaskContinuationOptions.RunContinuationsAsynchronously, _foregroundDispatcher.ForegroundScheduler); + _computingTagHelpers = (_projectSnapshot, task); + } + + private void TagHelpersUpdated(Task> 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(); var handler = ContextChanged; if (handler != null) { handler(this, new ContextChangeEventArgs(kind)); } + + if (kind == ContextChangeKind.ProjectChanged && + _projectSnapshot != null && + _computingTagHelpers.project == null) + { + StartComputingTagHelpers(); + } } // Internal for testing internal void ProjectManager_Changed(object sender, ProjectChangeEventArgs e) { + _foregroundDispatcher.AssertForegroundThread(); + if (_projectPath != null && - string.Equals(_projectPath, e.Project.FilePath, StringComparison.OrdinalIgnoreCase)) + string.Equals(_projectPath, e.ProjectFilePath, StringComparison.OrdinalIgnoreCase)) { - if (e.Kind == ProjectChangeKind.TagHelpersChanged) + // This will be the new snapshot unless the project was removed. + _projectSnapshot = _projectManager.GetLoadedProject(e.ProjectFilePath); + + switch (e.Kind) { - OnContextChanged(e.Project, ContextChangeKind.TagHelpersChanged); - } - else - { - OnContextChanged(e.Project, ContextChangeKind.ProjectChanged); + case ProjectChangeKind.DocumentsChanged: + + // Nothing to do. + break; + + case ProjectChangeKind.ProjectAdded: + case ProjectChangeKind.ProjectChanged: + + // Just an update + OnContextChanged(ContextChangeKind.ProjectChanged); + break; + + case ProjectChangeKind.ProjectRemoved: + + // Fall back to ephemeral project + _projectSnapshot = _projectManager.GetOrCreateProject(ProjectPath); + OnContextChanged(ContextChangeKind.ProjectChanged); + break; + + case ProjectChangeKind.DocumentContentChanged: + + // Do nothing + break; + + default: + throw new InvalidOperationException($"Unknown ProjectChangeKind {e.Kind}"); } } } @@ -212,17 +313,21 @@ namespace Microsoft.VisualStudio.Editor.Razor // Internal for testing internal void EditorSettingsManager_Changed(object sender, EditorSettingsChangedEventArgs args) { - OnContextChanged(_project, ContextChangeKind.EditorSettingsChanged); + _foregroundDispatcher.AssertForegroundThread(); + + OnContextChanged(ContextChangeKind.EditorSettingsChanged); } // Internal for testing internal void Import_Changed(object sender, ImportChangedEventArgs args) { + _foregroundDispatcher.AssertForegroundThread(); + foreach (var path in args.AssociatedDocuments) { if (string.Equals(_filePath, path, StringComparison.OrdinalIgnoreCase)) { - OnContextChanged(_project, ContextChangeKind.ImportsChanged); + OnContextChanged(ContextChangeKind.ImportsChanged); break; } } diff --git a/src/Microsoft.VisualStudio.Editor.Razor/DefaultVisualStudioRazorParser.cs b/src/Microsoft.VisualStudio.Editor.Razor/DefaultVisualStudioRazorParser.cs index ddbd000f0b..0feda57bce 100644 --- a/src/Microsoft.VisualStudio.Editor.Razor/DefaultVisualStudioRazorParser.cs +++ b/src/Microsoft.VisualStudio.Editor.Razor/DefaultVisualStudioRazorParser.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Threading; using System.Threading.Tasks; @@ -31,7 +32,7 @@ namespace Microsoft.VisualStudio.Editor.Razor private readonly VisualStudioCompletionBroker _completionBroker; private readonly VisualStudioDocumentTracker _documentTracker; private readonly ForegroundDispatcher _dispatcher; - private readonly RazorProjectEngineFactoryService _projectEngineFactory; + private readonly ProjectSnapshotProjectEngineFactory _projectEngineFactory; private readonly ErrorReporter _errorReporter; private RazorProjectEngine _projectEngine; private RazorCodeDocument _codeDocument; @@ -47,7 +48,7 @@ namespace Microsoft.VisualStudio.Editor.Razor public DefaultVisualStudioRazorParser( ForegroundDispatcher dispatcher, VisualStudioDocumentTracker documentTracker, - RazorProjectEngineFactoryService projectEngineFactory, + ProjectSnapshotProjectEngineFactory projectEngineFactory, ErrorReporter errorReporter, VisualStudioCompletionBroker completionBroker) { @@ -167,8 +168,18 @@ namespace Microsoft.VisualStudio.Editor.Razor { _dispatcher.AssertForegroundThread(); + // Make sure any tests use the real thing or a good mock. These tests can cause failures + // that are hard to understand when this throws. + Debug.Assert(_documentTracker.IsSupportedProject); + Debug.Assert(_documentTracker.ProjectSnapshot != null); + + _projectEngine = _projectEngineFactory.Create(_documentTracker.ProjectSnapshot, ConfigureProjectEngine); + + Debug.Assert(_projectEngine != null); + Debug.Assert(_projectEngine.Engine != null); + Debug.Assert(_projectEngine.FileSystem != null); + var projectDirectory = Path.GetDirectoryName(_documentTracker.ProjectPath); - _projectEngine = _projectEngineFactory.Create(projectDirectory, ConfigureProjectEngine); _parser = new BackgroundParser(_projectEngine, FilePath, projectDirectory); _parser.ResultsReady += OnResultsReady; _parser.Start(); diff --git a/src/Microsoft.VisualStudio.Editor.Razor/DefaultVisualStudioRazorParserFactory.cs b/src/Microsoft.VisualStudio.Editor.Razor/DefaultVisualStudioRazorParserFactory.cs index 5318ede596..8807802090 100644 --- a/src/Microsoft.VisualStudio.Editor.Razor/DefaultVisualStudioRazorParserFactory.cs +++ b/src/Microsoft.VisualStudio.Editor.Razor/DefaultVisualStudioRazorParserFactory.cs @@ -9,7 +9,7 @@ namespace Microsoft.VisualStudio.Editor.Razor internal class DefaultVisualStudioRazorParserFactory : VisualStudioRazorParserFactory { private readonly ForegroundDispatcher _dispatcher; - private readonly RazorProjectEngineFactoryService _projectEngineFactoryService; + private readonly ProjectSnapshotProjectEngineFactory _projectEngineFactory; private readonly VisualStudioCompletionBroker _completionBroker; private readonly ErrorReporter _errorReporter; @@ -17,7 +17,7 @@ namespace Microsoft.VisualStudio.Editor.Razor ForegroundDispatcher dispatcher, ErrorReporter errorReporter, VisualStudioCompletionBroker completionBroker, - RazorProjectEngineFactoryService projectEngineFactoryService) + ProjectSnapshotProjectEngineFactory projectEngineFactory) { if (dispatcher == null) { @@ -34,15 +34,15 @@ namespace Microsoft.VisualStudio.Editor.Razor throw new ArgumentNullException(nameof(completionBroker)); } - if (projectEngineFactoryService == null) + if (projectEngineFactory == null) { - throw new ArgumentNullException(nameof(projectEngineFactoryService)); + throw new ArgumentNullException(nameof(projectEngineFactory)); } _dispatcher = dispatcher; _errorReporter = errorReporter; _completionBroker = completionBroker; - _projectEngineFactoryService = projectEngineFactoryService; + _projectEngineFactory = projectEngineFactory; } public override VisualStudioRazorParser Create(VisualStudioDocumentTracker documentTracker) @@ -57,7 +57,7 @@ namespace Microsoft.VisualStudio.Editor.Razor var parser = new DefaultVisualStudioRazorParser( _dispatcher, documentTracker, - _projectEngineFactoryService, + _projectEngineFactory, _errorReporter, _completionBroker); return parser; diff --git a/src/Microsoft.VisualStudio.Editor.Razor/DefaultVisualStudioRazorParserFactoryFactory.cs b/src/Microsoft.VisualStudio.Editor.Razor/DefaultVisualStudioRazorParserFactoryFactory.cs index 38cfe5f189..4e0ff7406e 100644 --- a/src/Microsoft.VisualStudio.Editor.Razor/DefaultVisualStudioRazorParserFactoryFactory.cs +++ b/src/Microsoft.VisualStudio.Editor.Razor/DefaultVisualStudioRazorParserFactoryFactory.cs @@ -35,13 +35,13 @@ namespace Microsoft.VisualStudio.Editor.Razor var workspaceServices = languageServices.WorkspaceServices; var errorReporter = workspaceServices.GetRequiredService(); var completionBroker = languageServices.GetRequiredService(); - var projectEngineFactoryService = languageServices.GetRequiredService(); + var projectEngineFactory = workspaceServices.GetRequiredService(); return new DefaultVisualStudioRazorParserFactory( _foregroundDispatcher, errorReporter, completionBroker, - projectEngineFactoryService); + projectEngineFactory); } } } \ No newline at end of file diff --git a/src/Microsoft.VisualStudio.Editor.Razor/VisualStudioDocumentTracker.cs b/src/Microsoft.VisualStudio.Editor.Razor/VisualStudioDocumentTracker.cs index 7cde4da716..57c46bfc52 100644 --- a/src/Microsoft.VisualStudio.Editor.Razor/VisualStudioDocumentTracker.cs +++ b/src/Microsoft.VisualStudio.Editor.Razor/VisualStudioDocumentTracker.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using Microsoft.AspNetCore.Razor.Language; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Razor.Editor; +using Microsoft.CodeAnalysis.Razor.ProjectSystem; using Microsoft.VisualStudio.Text; using Microsoft.VisualStudio.Text.Editor; @@ -29,6 +30,8 @@ namespace Microsoft.VisualStudio.Editor.Razor public abstract Project Project { get; } + internal abstract ProjectSnapshot ProjectSnapshot { get; } + public abstract Workspace Workspace { get; } public abstract ITextBuffer TextBuffer { get; } diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/DefaultRazorEngineDirectiveResolver.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/DefaultRazorEngineDirectiveResolver.cs deleted file mode 100644 index 9c1eadae88..0000000000 --- a/src/Microsoft.VisualStudio.LanguageServices.Razor/DefaultRazorEngineDirectiveResolver.cs +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -#if RAZOR_EXTENSION_DEVELOPER_MODE -using System; -using System.Collections.Generic; -using System.Composition; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Razor.Language; -using Microsoft.CodeAnalysis; - -namespace Microsoft.VisualStudio.LanguageServices.Razor -{ - [Export(typeof(IRazorEngineDirectiveResolver))] - internal class DefaultRazorEngineDirectiveResolver : IRazorEngineDirectiveResolver - { - public async Task> GetRazorEngineDirectivesAsync(Workspace workspace, Project project, CancellationToken cancellationToken = default(CancellationToken)) - { - try - { - var client = await RazorLanguageServiceClientFactory.CreateAsync(workspace, cancellationToken); - - using (var session = await client.CreateSessionAsync(project.Solution)) - { - var directives = await session.InvokeAsync>("GetDirectivesAsync", new object[] { project.Id.Id, "Foo", }, cancellationToken).ConfigureAwait(false); - return directives; - } - } - catch (Exception exception) - { - throw new InvalidOperationException( - Resources.FormatUnexpectedException( - typeof(DefaultRazorEngineDirectiveResolver).FullName, - nameof(GetRazorEngineDirectivesAsync)), - exception); - } - } - } -} -#endif \ No newline at end of file diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/DefaultRazorEngineDocumentGenerator.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/DefaultRazorEngineDocumentGenerator.cs deleted file mode 100644 index b7c0cce061..0000000000 --- a/src/Microsoft.VisualStudio.LanguageServices.Razor/DefaultRazorEngineDocumentGenerator.cs +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -#if RAZOR_EXTENSION_DEVELOPER_MODE -using System; -using System.Composition; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.CodeAnalysis; - -namespace Microsoft.VisualStudio.LanguageServices.Razor -{ - [Export(typeof(IRazorEngineDocumentGenerator))] - internal class DefaultRazorEngineDocumentGenerator : IRazorEngineDocumentGenerator - { - public async Task GenerateDocumentAsync(Workspace workspace, Project project, string filePath, string text, CancellationToken cancellationToken = default(CancellationToken)) - { - try - { - var client = await RazorLanguageServiceClientFactory.CreateAsync(workspace, cancellationToken); - - using (var session = await client.CreateSessionAsync(project.Solution)) - { - var document = await session.InvokeAsync("GenerateDocumentAsync", new object[] { project.Id.Id, "Foo", filePath, text }, cancellationToken).ConfigureAwait(false); - return document; - } - } - catch (Exception exception) - { - throw new InvalidOperationException( - Resources.FormatUnexpectedException( - typeof(DefaultRazorEngineDocumentGenerator).FullName, - nameof(GenerateDocumentAsync)), - exception); - } - } - } -} -#endif \ No newline at end of file diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/IRazorEngineDirectiveResolver.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/IRazorEngineDirectiveResolver.cs deleted file mode 100644 index c884eedca1..0000000000 --- a/src/Microsoft.VisualStudio.LanguageServices.Razor/IRazorEngineDirectiveResolver.cs +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -#if RAZOR_EXTENSION_DEVELOPER_MODE -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Razor.Language; -using Microsoft.CodeAnalysis; - -namespace Microsoft.VisualStudio.LanguageServices.Razor -{ - internal interface IRazorEngineDirectiveResolver - { - Task> GetRazorEngineDirectivesAsync(Workspace workspace, Project project, CancellationToken cancellationToken = default(CancellationToken)); - } -} -#endif \ No newline at end of file diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/IRazorEngineDocumentGenerator.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/IRazorEngineDocumentGenerator.cs deleted file mode 100644 index 4b39c82bdb..0000000000 --- a/src/Microsoft.VisualStudio.LanguageServices.Razor/IRazorEngineDocumentGenerator.cs +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -#if RAZOR_EXTENSION_DEVELOPER_MODE -using System.Threading; -using System.Threading.Tasks; -using Microsoft.CodeAnalysis; - -namespace Microsoft.VisualStudio.LanguageServices.Razor -{ - internal interface IRazorEngineDocumentGenerator - { - Task GenerateDocumentAsync(Workspace workspace, Project project, string filePath, string text, CancellationToken cancellationToken = default(CancellationToken)); - } -} -#endif \ No newline at end of file diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/Microsoft.VisualStudio.LanguageServices.Razor.csproj b/src/Microsoft.VisualStudio.LanguageServices.Razor/Microsoft.VisualStudio.LanguageServices.Razor.csproj index a3bbd0f3c9..26d1ac3025 100644 --- a/src/Microsoft.VisualStudio.LanguageServices.Razor/Microsoft.VisualStudio.LanguageServices.Razor.csproj +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/Microsoft.VisualStudio.LanguageServices.Razor.csproj @@ -43,7 +43,7 @@ -