From fafdd7e3af6d44904f7faee867ba1e8c2b047e70 Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Tue, 8 May 2018 22:51:17 -0700 Subject: [PATCH] Track the open/closed state of documents This change intoduces content changes to our project snapshots. We now know the open/closed state of documents that are initialized by the Razor project system and listen to the correct data source based on whether the file is open in the editor. There are a few other random improvements in here as well like a workaround for the upcoming name change to our OOP client type. --- .../BackgroundDocumentGenerator.cs | 57 ++-- .../DocumentKey.cs | 41 +++ .../ProjectSystem/DefaultDocumentSnapshot.cs | 22 ++ .../DefaultProjectSnapshotManager.cs | 214 ++++++++++++++- .../DocumentGeneratedOutputTracker.cs | 19 +- .../ProjectSystem/DocumentSnapshot.cs | 10 + .../ProjectSystem/DocumentState.cs | 164 +++++++++-- .../ProjectSystem/HostDocument.cs | 33 +-- .../ProjectSystem/ProjectChangeEventArgs.cs | 19 ++ .../ProjectSystem/ProjectChangeKind.cs | 7 +- .../ProjectSystem/ProjectDifference.cs | 4 +- .../ProjectSystem/ProjectSnapshotManager.cs | 2 + .../ProjectSnapshotManagerBase.cs | 12 +- .../ProjectSystem/ProjectState.cs | 90 ++++++- .../WorkspaceProjectSnapshotChangeTrigger.cs | 39 ++- .../Properties/AssemblyInfo.cs | 1 + .../DefaultVisualStudioDocumentTracker.cs | 9 +- .../Documents/EditorDocument.cs | 162 +++++++++++ .../Documents/EditorDocumentManager.cs | 25 ++ .../Documents/EditorDocumentManagerBase.cs | 221 +++++++++++++++ .../EditorDocumentManagerListener.cs | 100 +++++++ .../Documents/SnapshotChangeTracker.cs | 61 +++++ .../RunningDocumentTableEventSink.cs | 62 +++++ .../VisualStudioEditorDocumentManager.cs | 254 ++++++++++++++++++ ...isualStudioEditorDocumentManagerFactory.cs | 60 +++++ .../Documents/VsTextBufferDataEventsSink.cs | 56 ++++ ...VisualStudio.LanguageServices.Razor.csproj | 13 +- .../OOPTagHelperResolverFactory.cs | 30 +++ .../ProjectSystem/RazorProjectHostBase.cs | 2 +- .../DefaultProjectSnapshotTest.cs | 18 +- .../ProjectSystem/DocumentStateTest.cs | 127 ++++++++- .../ProjectSystem/ProjectStateTest.cs | 225 ++++++++++++---- .../StringTextImage.cs | 70 +++++ .../StringTextSnapshot.cs | 12 +- ...ProjectSnapshotProjectEngineFactoryTest.cs | 10 +- .../EditorDocumentManagerBaseTest.cs | 212 +++++++++++++++ .../Documents/EditorDocumentTest.cs | 110 ++++++++ .../Infrastructure/TestTextBuffer.cs | 9 + .../BackgroundDocumentGeneratorTest.cs | 8 +- .../DefaultProjectSnapshotManagerTest.cs | 199 ++++++++++++-- ...rkspaceProjectSnapshotChangeTriggerTest.cs | 27 +- ...UpdatesProjectSnapshotChangeTriggerTest.cs | 12 +- .../ProjectBuildChangeTriggerTest.cs | 12 +- ...crosoft.VisualStudio.RazorExtension.csproj | 22 +- .../RazorInfo/DirectiveCollectionViewModel.cs | 35 +++ ...ViewModel.cs => DirectiveItemViewModel.cs} | 4 +- .../RazorInfo/DocumentCollectionViewModel.cs | 81 ++++++ .../RazorInfo/DocumentItemViewModel.cs | 70 +++++ .../RazorInfo/DocumentSnapshotViewModel.cs | 50 ---- .../RazorInfo/NullToEnabledConverter.cs | 30 +++ .../RazorInfo/ProjectInfoViewModel.cs | 62 ----- .../ProjectPropertyCollectionViewModel.cs | 41 +++ ...del.cs => ProjectPropertyItemViewModel.cs} | 4 +- .../RazorInfo/ProjectSnapshotViewModel.cs | 55 ---- .../RazorInfo/ProjectViewModel.cs | 15 -- .../RazorInfo/RazorInfoToolWindow.cs | 75 +----- .../RazorInfo/RazorInfoToolWindowControl.xaml | 32 ++- .../RazorInfo/RazorInfoViewModel.cs | 200 +++++++++----- .../RazorInfo/TagHelperCollectionViewModel.cs | 72 +++++ .../RazorInfo/TagHelperItemViewModel.cs | 28 ++ .../RazorInfo/TagHelperViewModel.cs | 26 -- 61 files changed, 3102 insertions(+), 640 deletions(-) create mode 100644 src/Microsoft.CodeAnalysis.Razor.Workspaces/DocumentKey.cs create mode 100644 src/Microsoft.VisualStudio.Editor.Razor/Documents/EditorDocument.cs create mode 100644 src/Microsoft.VisualStudio.Editor.Razor/Documents/EditorDocumentManager.cs create mode 100644 src/Microsoft.VisualStudio.Editor.Razor/Documents/EditorDocumentManagerBase.cs create mode 100644 src/Microsoft.VisualStudio.Editor.Razor/Documents/EditorDocumentManagerListener.cs create mode 100644 src/Microsoft.VisualStudio.Editor.Razor/Documents/SnapshotChangeTracker.cs create mode 100644 src/Microsoft.VisualStudio.LanguageServices.Razor/Documents/RunningDocumentTableEventSink.cs create mode 100644 src/Microsoft.VisualStudio.LanguageServices.Razor/Documents/VisualStudioEditorDocumentManager.cs create mode 100644 src/Microsoft.VisualStudio.LanguageServices.Razor/Documents/VisualStudioEditorDocumentManagerFactory.cs create mode 100644 src/Microsoft.VisualStudio.LanguageServices.Razor/Documents/VsTextBufferDataEventsSink.cs create mode 100644 test/Microsoft.VisualStudio.Editor.Razor.Test.Common/StringTextImage.cs create mode 100644 test/Microsoft.VisualStudio.Editor.Razor.Test/Documents/EditorDocumentManagerBaseTest.cs create mode 100644 test/Microsoft.VisualStudio.Editor.Razor.Test/Documents/EditorDocumentTest.cs rename test/{Microsoft.CodeAnalysis.Razor.Workspaces.Test => Microsoft.VisualStudio.LanguageServices.Razor.Test}/ProjectSystem/WorkspaceProjectSnapshotChangeTriggerTest.cs (93%) create mode 100644 tooling/Microsoft.VisualStudio.RazorExtension/RazorInfo/DirectiveCollectionViewModel.cs rename tooling/Microsoft.VisualStudio.RazorExtension/RazorInfo/{DirectiveDescriptorViewModel.cs => DirectiveItemViewModel.cs} (87%) create mode 100644 tooling/Microsoft.VisualStudio.RazorExtension/RazorInfo/DocumentCollectionViewModel.cs create mode 100644 tooling/Microsoft.VisualStudio.RazorExtension/RazorInfo/DocumentItemViewModel.cs delete mode 100644 tooling/Microsoft.VisualStudio.RazorExtension/RazorInfo/DocumentSnapshotViewModel.cs create mode 100644 tooling/Microsoft.VisualStudio.RazorExtension/RazorInfo/NullToEnabledConverter.cs delete mode 100644 tooling/Microsoft.VisualStudio.RazorExtension/RazorInfo/ProjectInfoViewModel.cs create mode 100644 tooling/Microsoft.VisualStudio.RazorExtension/RazorInfo/ProjectPropertyCollectionViewModel.cs rename tooling/Microsoft.VisualStudio.RazorExtension/RazorInfo/{PropertyViewModel.cs => ProjectPropertyItemViewModel.cs} (74%) delete mode 100644 tooling/Microsoft.VisualStudio.RazorExtension/RazorInfo/ProjectSnapshotViewModel.cs create mode 100644 tooling/Microsoft.VisualStudio.RazorExtension/RazorInfo/TagHelperCollectionViewModel.cs create mode 100644 tooling/Microsoft.VisualStudio.RazorExtension/RazorInfo/TagHelperItemViewModel.cs delete mode 100644 tooling/Microsoft.VisualStudio.RazorExtension/RazorInfo/TagHelperViewModel.cs diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/DocumentGenerator/BackgroundDocumentGenerator.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/DocumentGenerator/BackgroundDocumentGenerator.cs index 6fd591d1ca..16d7f11959 100644 --- a/src/Microsoft.CodeAnalysis.Razor.Workspaces/DocumentGenerator/BackgroundDocumentGenerator.cs +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/DocumentGenerator/BackgroundDocumentGenerator.cs @@ -8,17 +8,17 @@ 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. + // [Export(typeof(ProjectSnapshotChangeTrigger))] internal class BackgroundDocumentGenerator : ProjectSnapshotChangeTrigger { private ForegroundDispatcher _foregroundDispatcher; private ProjectSnapshotManagerBase _projectManager; - private readonly Dictionary _files; + private readonly Dictionary _files; private Timer _timer; [ImportingConstructor] @@ -31,7 +31,7 @@ namespace Microsoft.CodeAnalysis.Razor _foregroundDispatcher = foregroundDispatcher; - _files = new Dictionary(); + _files = new Dictionary(); } public bool HasPendingNotifications @@ -127,7 +127,7 @@ namespace Microsoft.CodeAnalysis.Razor { // 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); + _files[new DocumentKey(project.FilePath, document.FilePath)] = document; StartWorker(); } @@ -217,7 +217,6 @@ namespace Microsoft.CodeAnalysis.Razor { case ProjectChangeKind.ProjectAdded: case ProjectChangeKind.ProjectChanged: - case ProjectChangeKind.DocumentsChanged: { var project = _projectManager.GetLoadedProject(e.ProjectFilePath); foreach (var documentFilePath in project.DocumentFilePaths) @@ -228,12 +227,21 @@ namespace Microsoft.CodeAnalysis.Razor break; } - case ProjectChangeKind.DocumentContentChanged: + case ProjectChangeKind.ProjectRemoved: + // ignore + break; + + case ProjectChangeKind.DocumentAdded: + case ProjectChangeKind.DocumentChanged: { - throw null; + var project = _projectManager.GetLoadedProject(e.ProjectFilePath); + Enqueue(project, project.GetDocument(e.DocumentFilePath)); + + break; } - case ProjectChangeKind.ProjectRemoved: + + case ProjectChangeKind.DocumentRemoved: // ignore break; @@ -241,38 +249,5 @@ namespace Microsoft.CodeAnalysis.Razor 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/DocumentKey.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/DocumentKey.cs new file mode 100644 index 0000000000..2fe707a707 --- /dev/null +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/DocumentKey.cs @@ -0,0 +1,41 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.Extensions.Internal; + +namespace Microsoft.CodeAnalysis.Razor +{ + public struct DocumentKey : IEquatable + { + public DocumentKey(string projectFilePath, string documentFilePath) + { + ProjectFilePath = projectFilePath; + DocumentFilePath = documentFilePath; + } + + public string ProjectFilePath { get; } + + public string DocumentFilePath { get; } + + public bool Equals(DocumentKey other) + { + return + FilePathComparer.Instance.Equals(ProjectFilePath, other.ProjectFilePath) && + FilePathComparer.Instance.Equals(DocumentFilePath, other.DocumentFilePath); + } + + public override bool Equals(object obj) + { + return obj is DocumentKey key ? Equals(key) : false; + } + + public override int GetHashCode() + { + var hash = new HashCodeCombiner(); + hash.Add(ProjectFilePath, FilePathComparer.Instance); + hash.Add(DocumentFilePath, FilePathComparer.Instance); + return hash; + } + } +} diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultDocumentSnapshot.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultDocumentSnapshot.cs index 6ce3edb596..0cc4512f2e 100644 --- a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultDocumentSnapshot.cs +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultDocumentSnapshot.cs @@ -2,8 +2,10 @@ // 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; using Microsoft.AspNetCore.Razor.Language; +using Microsoft.CodeAnalysis.Text; namespace Microsoft.CodeAnalysis.Razor.ProjectSystem { @@ -33,12 +35,32 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem public override string TargetPath => State.HostDocument.TargetPath; + public override Task GetTextAsync() + { + return State.GetTextAsync(); + } + + public override Task GetTextVersionAsync() + { + return State.GetTextVersionAsync(); + } + 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 TryGetText(out SourceText result) + { + return State.TryGetText(out result); + } + + public override bool TryGetTextVersion(out VersionStamp result) + { + return State.TryGetTextVersion(out result); + } + public override bool TryGetGeneratedOutput(out RazorCodeDocument result) { if (State.GeneratedOutput.IsResultAvailable) diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultProjectSnapshotManager.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultProjectSnapshotManager.cs index 174b669c86..5a810cc803 100644 --- a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultProjectSnapshotManager.cs +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultProjectSnapshotManager.cs @@ -4,6 +4,9 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Text; namespace Microsoft.CodeAnalysis.Razor.ProjectSystem { @@ -34,6 +37,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem // Each entry holds a ProjectState and an optional ProjectSnapshot. ProjectSnapshots are // created lazily. private readonly Dictionary _projects; + private readonly HashSet _openDocuments; public DefaultProjectSnapshotManager( ForegroundDispatcher foregroundDispatcher, @@ -67,6 +71,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem Workspace = workspace; _projects = new Dictionary(FilePathComparer.Instance); + _openDocuments = new HashSet(FilePathComparer.Instance); for (var i = 0; i < _triggers.Length; i++) { @@ -133,7 +138,19 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem return GetLoadedProject(filePath) ?? new EphemeralProjectSnapshot(Workspace.Services, filePath); } - public override void DocumentAdded(HostProject hostProject, HostDocument document) + public override bool IsDocumentOpen(string documentFilePath) + { + if (documentFilePath == null) + { + throw new ArgumentNullException(nameof(documentFilePath)); + } + + _foregroundDispatcher.AssertForegroundThread(); + + return _openDocuments.Contains(documentFilePath); + } + + public override void DocumentAdded(HostProject hostProject, HostDocument document, TextLoader textLoader) { if (hostProject == null) { @@ -149,13 +166,17 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem if (_projects.TryGetValue(hostProject.FilePath, out var entry)) { - var state = entry.State.WithAddedHostDocument(document); + var loader = textLoader == null ? DocumentState.EmptyLoader : (Func>)(() => + { + return textLoader.LoadTextAndVersionAsync(Workspace, null, CancellationToken.None); + }); + var state = entry.State.WithAddedHostDocument(document, loader); // Document updates can no-op. if (!object.ReferenceEquals(state, entry.State)) { _projects[hostProject.FilePath] = new Entry(state); - NotifyListeners(new ProjectChangeEventArgs(hostProject.FilePath, ProjectChangeKind.DocumentsChanged)); + NotifyListeners(new ProjectChangeEventArgs(hostProject.FilePath, document.FilePath, ProjectChangeKind.DocumentAdded)); } } } @@ -181,7 +202,187 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem if (!object.ReferenceEquals(state, entry.State)) { _projects[hostProject.FilePath] = new Entry(state); - NotifyListeners(new ProjectChangeEventArgs(hostProject.FilePath, ProjectChangeKind.DocumentsChanged)); + NotifyListeners(new ProjectChangeEventArgs(hostProject.FilePath, document.FilePath, ProjectChangeKind.DocumentRemoved)); + } + } + } + + public override void DocumentOpened(string projectFilePath, string documentFilePath, SourceText sourceText) + { + if (projectFilePath == null) + { + throw new ArgumentNullException(nameof(projectFilePath)); + } + + if (documentFilePath == null) + { + throw new ArgumentNullException(nameof(documentFilePath)); + } + + if (sourceText == null) + { + throw new ArgumentNullException(nameof(sourceText)); + } + + _foregroundDispatcher.AssertForegroundThread(); + if (_projects.TryGetValue(projectFilePath, out var entry) && + entry.State.Documents.TryGetValue(documentFilePath, out var older)) + { + ProjectState state; + SourceText olderText; + VersionStamp olderVersion; + + var currentText = sourceText; + if (older.TryGetText(out olderText) && + older.TryGetTextVersion(out olderVersion)) + { + var version = currentText.ContentEquals(olderText) ? olderVersion : olderVersion.GetNewerVersion(); + state = entry.State.WithChangedHostDocument(older.HostDocument, currentText, version); + } + else + { + state = entry.State.WithChangedHostDocument(older.HostDocument, async () => + { + olderText = await older.GetTextAsync().ConfigureAwait(false); + olderVersion = await older.GetTextVersionAsync().ConfigureAwait(false); + + var version = currentText.ContentEquals(olderText) ? olderVersion : olderVersion.GetNewerVersion(); + return TextAndVersion.Create(currentText, version, documentFilePath); + }); + } + + _openDocuments.Add(documentFilePath); + + // Document updates can no-op. + if (!object.ReferenceEquals(state, entry.State)) + { + _projects[projectFilePath] = new Entry(state); + NotifyListeners(new ProjectChangeEventArgs(projectFilePath, documentFilePath, ProjectChangeKind.DocumentChanged)); + } + } + } + + public override void DocumentClosed(string projectFilePath, string documentFilePath, TextLoader textLoader) + { + if (projectFilePath == null) + { + throw new ArgumentNullException(nameof(projectFilePath)); + } + + if (documentFilePath == null) + { + throw new ArgumentNullException(nameof(documentFilePath)); + } + + if (textLoader == null) + { + throw new ArgumentNullException(nameof(textLoader)); + } + + _foregroundDispatcher.AssertForegroundThread(); + if (_projects.TryGetValue(projectFilePath, out var entry) && + entry.State.Documents.TryGetValue(documentFilePath, out var older)) + { + var state = entry.State.WithChangedHostDocument(older.HostDocument, async () => + { + return await textLoader.LoadTextAndVersionAsync(Workspace, default, default); + }); + + _openDocuments.Remove(documentFilePath); + + // Document updates can no-op. + if (!object.ReferenceEquals(state, entry.State)) + { + _projects[projectFilePath] = new Entry(state); + NotifyListeners(new ProjectChangeEventArgs(projectFilePath, documentFilePath, ProjectChangeKind.DocumentChanged)); + } + } + } + + public override void DocumentChanged(string projectFilePath, string documentFilePath, SourceText sourceText) + { + if (projectFilePath == null) + { + throw new ArgumentNullException(nameof(projectFilePath)); + } + + if (documentFilePath == null) + { + throw new ArgumentNullException(nameof(documentFilePath)); + } + + if (sourceText == null) + { + throw new ArgumentNullException(nameof(sourceText)); + } + + _foregroundDispatcher.AssertForegroundThread(); + if (_projects.TryGetValue(projectFilePath, out var entry) && + entry.State.Documents.TryGetValue(documentFilePath, out var older)) + { + ProjectState state; + SourceText olderText; + VersionStamp olderVersion; + + var currentText = sourceText; + if (older.TryGetText(out olderText) && + older.TryGetTextVersion(out olderVersion)) + { + var version = currentText.ContentEquals(olderText) ? olderVersion : olderVersion.GetNewerVersion(); + state = entry.State.WithChangedHostDocument(older.HostDocument, currentText, version); + } + else + { + state = entry.State.WithChangedHostDocument(older.HostDocument, async () => + { + olderText = await older.GetTextAsync().ConfigureAwait(false); + olderVersion = await older.GetTextVersionAsync().ConfigureAwait(false); + + var version = currentText.ContentEquals(olderText) ? olderVersion : olderVersion.GetNewerVersion(); + return TextAndVersion.Create(currentText, version, documentFilePath); + }); + } + + // Document updates can no-op. + if (!object.ReferenceEquals(state, entry.State)) + { + _projects[projectFilePath] = new Entry(state); + NotifyListeners(new ProjectChangeEventArgs(projectFilePath, documentFilePath, ProjectChangeKind.DocumentChanged)); + } + } + } + + public override void DocumentChanged(string projectFilePath, string documentFilePath, TextLoader textLoader) + { + if (projectFilePath == null) + { + throw new ArgumentNullException(nameof(projectFilePath)); + } + + if (documentFilePath == null) + { + throw new ArgumentNullException(nameof(documentFilePath)); + } + + if (textLoader == null) + { + throw new ArgumentNullException(nameof(textLoader)); + } + + _foregroundDispatcher.AssertForegroundThread(); + if (_projects.TryGetValue(projectFilePath, out var entry) && + entry.State.Documents.TryGetValue(documentFilePath, out var older)) + { + var state = entry.State.WithChangedHostDocument(older.HostDocument, async () => + { + return await textLoader.LoadTextAndVersionAsync(Workspace, default, default); + }); + + // Document updates can no-op. + if (!object.ReferenceEquals(state, entry.State)) + { + _projects[projectFilePath] = new Entry(state); + NotifyListeners(new ProjectChangeEventArgs(projectFilePath, documentFilePath, ProjectChangeKind.DocumentChanged)); } } } @@ -205,7 +406,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem // So if possible find a WorkspaceProject. var workspaceProject = GetWorkspaceProject(hostProject.FilePath); - var state = new ProjectState(Workspace.Services, hostProject, workspaceProject); + var state = ProjectState.Create(Workspace.Services, hostProject, workspaceProject); _projects[hostProject.FilePath] = new Entry(state); // We need to notify listeners about every project add. @@ -300,7 +501,8 @@ 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 entry) && - (entry.State.WorkspaceProject == null || entry.State.WorkspaceProject.Id == workspaceProject.Id)) + (entry.State.WorkspaceProject == null || entry.State.WorkspaceProject.Id == workspaceProject.Id) && + (entry.State.WorkspaceProject == null || entry.State.WorkspaceProject.Version.GetNewerVersion(workspaceProject.Version) == workspaceProject.Version)) { var state = entry.State.WithWorkspaceProject(workspaceProject); diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DocumentGeneratedOutputTracker.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DocumentGeneratedOutputTracker.cs index e3114148b1..20329b954e 100644 --- a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DocumentGeneratedOutputTracker.cs +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DocumentGeneratedOutputTracker.cs @@ -10,11 +10,6 @@ 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; @@ -31,6 +26,8 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem public bool IsResultAvailable => _task?.IsCompleted == true; + public DocumentGeneratedOutputTracker Older => _older; + public Task GetGeneratedOutputInitializationTask(ProjectSnapshot project, DocumentSnapshot document) { if (project == null) @@ -57,18 +54,8 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem return _task; } - public DocumentGeneratedOutputTracker ForkFor(DocumentState state, ProjectDifference difference) + public DocumentGeneratedOutputTracker Fork() { - if (state == null) - { - throw new ArgumentNullException(nameof(state)); - } - - if ((difference & Mask) != 0) - { - return null; - } - return new DocumentGeneratedOutputTracker(this); } diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DocumentSnapshot.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DocumentSnapshot.cs index 5a86527bc9..ac94c6a1cd 100644 --- a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DocumentSnapshot.cs +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DocumentSnapshot.cs @@ -1,8 +1,10 @@ // 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.AspNetCore.Razor.Language; +using Microsoft.CodeAnalysis.Text; namespace Microsoft.CodeAnalysis.Razor.ProjectSystem { @@ -12,8 +14,16 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem public abstract string TargetPath { get; } + public abstract Task GetTextAsync(); + + public abstract Task GetTextVersionAsync(); + public abstract Task GetGeneratedOutputAsync(); + public abstract bool TryGetText(out SourceText result); + + public abstract bool TryGetTextVersion(out VersionStamp result); + public abstract bool TryGetGeneratedOutput(out RazorCodeDocument result); } } diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DocumentState.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DocumentState.cs index f53adf18b0..192a038f1d 100644 --- a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DocumentState.cs +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DocumentState.cs @@ -2,17 +2,33 @@ // 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.CodeAnalysis.Host; +using Microsoft.CodeAnalysis.Text; namespace Microsoft.CodeAnalysis.Razor.ProjectSystem { internal class DocumentState { + private static readonly TextAndVersion EmptyText = TextAndVersion.Create( + SourceText.From(string.Empty), + VersionStamp.Default); + + public static readonly Func> EmptyLoader = () => Task.FromResult(EmptyText); + private readonly object _lock; + private Func> _loader; + private Task _loaderTask; + private SourceText _sourceText; + private VersionStamp? _version; + private DocumentGeneratedOutputTracker _generatedOutput; - public DocumentState(HostWorkspaceServices services, HostDocument hostDocument) + public static DocumentState Create( + HostWorkspaceServices services, + HostDocument hostDocument, + Func> loader) { if (services == null) { @@ -24,33 +40,29 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem throw new ArgumentNullException(nameof(hostDocument)); } - Services = services; - HostDocument = hostDocument; - Version = VersionStamp.Create(); - - _lock = new object(); + loader = loader ?? EmptyLoader; + return new DocumentState(services, hostDocument, null, null, loader); } - public DocumentState(DocumentState previous, ProjectDifference difference) + private DocumentState( + HostWorkspaceServices services, + HostDocument hostDocument, + SourceText text, + VersionStamp? version, + Func> loader) { - if (previous == null) - { - throw new ArgumentNullException(nameof(previous)); - } - - Services = previous.Services; - HostDocument = previous.HostDocument; - Version = previous.Version.GetNewerVersion(); - - _generatedOutput = previous._generatedOutput?.ForkFor(this, difference); + Services = services; + HostDocument = hostDocument; + _sourceText = text; + _version = version; + _loader = loader; + _lock = new object(); } public HostDocument HostDocument { get; } public HostWorkspaceServices Services { get; } - public VersionStamp Version { get; } - public DocumentGeneratedOutputTracker GeneratedOutput { get @@ -69,5 +81,119 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem return _generatedOutput; } } + + public async Task GetTextAsync() + { + if (TryGetText(out var text)) + { + return text; + } + + lock (_lock) + { + _loaderTask = _loader(); + } + + return (await _loaderTask.ConfigureAwait(false)).Text; + } + + public async Task GetTextVersionAsync() + { + if (TryGetTextVersion(out var version)) + { + return version; + } + + lock (_lock) + { + _loaderTask = _loader(); + } + + return (await _loaderTask.ConfigureAwait(false)).Version; + } + + public bool TryGetText(out SourceText result) + { + if (_sourceText != null) + { + result = _sourceText; + return true; + } + + if (_loaderTask != null && _loaderTask.IsCompleted) + { + result = _loaderTask.Result.Text; + return true; + } + + result = null; + return false; + } + + public bool TryGetTextVersion(out VersionStamp result) + { + if (_version != null) + { + result = _version.Value; + return true; + } + + if (_loaderTask != null && _loaderTask.IsCompleted) + { + result = _loaderTask.Result.Version; + return true; + } + + result = default; + return false; + } + + public DocumentState WithConfigurationChange() + { + var state = new DocumentState(Services, HostDocument, _sourceText, _version, _loader); + + // The source could not have possibly changed. + state._sourceText = _sourceText; + state._version = _version; + state._loaderTask = _loaderTask; + + return state; + } + + public DocumentState WithWorkspaceProjectChange() + { + var state = new DocumentState(Services, HostDocument, _sourceText, _version, _loader); + + // The source could not have possibly changed. + state._sourceText = _sourceText; + state._version = _version; + state._loaderTask = _loaderTask; + + // Opportunistically cache the generated code + state._generatedOutput = _generatedOutput?.Fork(); + + return state; + } + + public DocumentState WithText(SourceText sourceText, VersionStamp version) + { + if (sourceText == null) + { + throw new ArgumentNullException(nameof(sourceText)); + } + + return new DocumentState(Services, HostDocument, sourceText, version, null); + } + + + public DocumentState WithTextLoader(Func> loader) + { + if (loader == null) + { + throw new ArgumentNullException(nameof(loader)); + } + + return new DocumentState(Services, HostDocument, null, null, loader); + } } } diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/HostDocument.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/HostDocument.cs index 146943cab6..859cc0df32 100644 --- a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/HostDocument.cs +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/HostDocument.cs @@ -2,11 +2,10 @@ // 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 + internal class HostDocument { public HostDocument(string filePath, string targetPath) { @@ -27,35 +26,5 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem 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 0c0faa3db8..a163ba5f1b 100644 --- a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectChangeEventArgs.cs +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectChangeEventArgs.cs @@ -9,12 +9,31 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem { public ProjectChangeEventArgs(string projectFilePath, ProjectChangeKind kind) { + if (projectFilePath == null) + { + throw new ArgumentNullException(nameof(projectFilePath)); + } + ProjectFilePath = projectFilePath; Kind = kind; } + public ProjectChangeEventArgs(string projectFilePath, string documentFilePath, ProjectChangeKind kind) + { + if (projectFilePath == null) + { + throw new ArgumentNullException(nameof(projectFilePath)); + } + + ProjectFilePath = projectFilePath; + DocumentFilePath = documentFilePath; + Kind = kind; + } + public string ProjectFilePath { get; } + public string DocumentFilePath { 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 53b6b36529..98bd0a4010 100644 --- a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectChangeKind.cs +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectChangeKind.cs @@ -8,7 +8,10 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem ProjectAdded, ProjectRemoved, ProjectChanged, - DocumentsChanged, - DocumentContentChanged, + DocumentAdded, + DocumentRemoved, + + // This could be a state change (opened/closed) or a content change. + DocumentChanged, } } diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectDifference.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectDifference.cs index d450393f3c..138acdb312 100644 --- a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectDifference.cs +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectDifference.cs @@ -13,6 +13,8 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem WorkspaceProjectAdded = 2, WorkspaceProjectRemoved = 4, WorkspaceProjectChanged = 8, - DocumentsChanged = 16, + DocumentAdded = 16, + DocumentRemoved = 32, + DocumentChanged = 64, } } diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSnapshotManager.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSnapshotManager.cs index e491674399..40dfec08a9 100644 --- a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSnapshotManager.cs +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSnapshotManager.cs @@ -13,6 +13,8 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem public abstract IReadOnlyList Projects { get; } + public abstract bool IsDocumentOpen(string documentFilePath); + 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 87306d1b5a..5545bbb3a8 100644 --- a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSnapshotManagerBase.cs +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSnapshotManagerBase.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using Microsoft.CodeAnalysis.Text; namespace Microsoft.CodeAnalysis.Razor.ProjectSystem { @@ -9,7 +10,16 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem { public abstract Workspace Workspace { get; } - public abstract void DocumentAdded(HostProject hostProject, HostDocument hostDocument); + public abstract void DocumentAdded(HostProject hostProject, HostDocument hostDocument, TextLoader textLoader); + + // Yeah this is kinda ugly. + public abstract void DocumentOpened(string projectFilePath, string documentFilePath, SourceText sourceText); + + public abstract void DocumentClosed(string projectFilePath, string documentFilePath, TextLoader textLoader); + + public abstract void DocumentChanged(string projectFilePath, string documentFilePath, TextLoader textLoader); + + public abstract void DocumentChanged(string projectFilePath, string documentFilePath, SourceText sourceText); public abstract void DocumentRemoved(HostProject hostProject, HostDocument hostDocument); diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectState.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectState.cs index 53e6cc3b15..5dece86c8f 100644 --- a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectState.cs +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectState.cs @@ -3,7 +3,9 @@ using System; using System.Collections.Generic; +using System.Threading.Tasks; using Microsoft.CodeAnalysis.Host; +using Microsoft.CodeAnalysis.Text; namespace Microsoft.CodeAnalysis.Razor.ProjectSystem { @@ -17,10 +19,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem private ProjectEngineTracker _projectEngine; private ProjectTagHelperTracker _tagHelpers; - public ProjectState( - HostWorkspaceServices services, - HostProject hostProject, - Project workspaceProject) + public static ProjectState Create(HostWorkspaceServices services, HostProject hostProject, Project workspaceProject = null) { if (services == null) { @@ -32,6 +31,14 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem throw new ArgumentNullException(nameof(hostProject)); } + return new ProjectState(services, hostProject, workspaceProject); + } + + private ProjectState( + HostWorkspaceServices services, + HostProject hostProject, + Project workspaceProject) + { Services = services; HostProject = hostProject; WorkspaceProject = workspaceProject; @@ -41,7 +48,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem _lock = new object(); } - public ProjectState( + private ProjectState( ProjectState older, ProjectDifference difference, HostProject hostProject, @@ -126,13 +133,18 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem } } - public ProjectState WithAddedHostDocument(HostDocument hostDocument) + public ProjectState WithAddedHostDocument(HostDocument hostDocument, Func> loader) { if (hostDocument == null) { throw new ArgumentNullException(nameof(hostDocument)); } + if (loader == null) + { + throw new ArgumentNullException(nameof(loader)); + } + // 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)) @@ -145,10 +157,10 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem { documents.Add(kvp.Key, kvp.Value); } + + documents.Add(hostDocument.FilePath, DocumentState.Create(Services, hostDocument, loader)); - documents.Add(hostDocument.FilePath, new DocumentState(Services, hostDocument)); - - var difference = ProjectDifference.DocumentsChanged; + var difference = ProjectDifference.DocumentAdded; var state = new ProjectState(this, difference, HostProject, WorkspaceProject, documents); return state; } @@ -173,11 +185,65 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem documents.Remove(hostDocument.FilePath); - var difference = ProjectDifference.DocumentsChanged; + var difference = ProjectDifference.DocumentRemoved; var state = new ProjectState(this, difference, HostProject, WorkspaceProject, documents); return state; } + public ProjectState WithChangedHostDocument(HostDocument hostDocument, SourceText sourceText, VersionStamp version) + { + 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); + } + + if (documents.TryGetValue(hostDocument.FilePath, out var document)) + { + documents[hostDocument.FilePath] = document.WithText(sourceText, version); + } + + var state = new ProjectState(this, ProjectDifference.DocumentChanged, HostProject, WorkspaceProject, documents); + return state; + } + + public ProjectState WithChangedHostDocument(HostDocument hostDocument, Func> loader) + { + 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); + } + + if (documents.TryGetValue(hostDocument.FilePath, out var document)) + { + documents[hostDocument.FilePath] = document.WithTextLoader(loader); + } + + var state = new ProjectState(this, ProjectDifference.DocumentChanged, HostProject, WorkspaceProject, documents); + return state; + } + public ProjectState WithHostProject(HostProject hostProject) { if (hostProject == null) @@ -194,7 +260,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem var documents = new Dictionary(FilePathComparer.Instance); foreach (var kvp in Documents) { - documents.Add(kvp.Key, new DocumentState(kvp.Value, difference)); + documents.Add(kvp.Key, kvp.Value.WithConfigurationChange()); } var state = new ProjectState(this, difference, hostProject, WorkspaceProject, documents); @@ -227,7 +293,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem var documents = new Dictionary(FilePathComparer.Instance); foreach (var kvp in Documents) { - documents.Add(kvp.Key, new DocumentState(kvp.Value, difference)); + documents.Add(kvp.Key, kvp.Value.WithConfigurationChange()); } var state = new ProjectState(this, difference, HostProject, workspaceProject, documents); diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/WorkspaceProjectSnapshotChangeTrigger.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/WorkspaceProjectSnapshotChangeTrigger.cs index fb2deeec32..f5bbee95de 100644 --- a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/WorkspaceProjectSnapshotChangeTrigger.cs +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/WorkspaceProjectSnapshotChangeTrigger.cs @@ -1,8 +1,10 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System.Collections.Generic; using System.Composition; using System.Diagnostics; +using System.Threading.Tasks; namespace Microsoft.CodeAnalysis.Razor.ProjectSystem { @@ -11,11 +13,21 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem { private ProjectSnapshotManagerBase _projectManager; + public int ProjectChangeDelay { get; set; } = 3 * 1000; + + // We throttle updates to projects to prevent doing too much work while the projects + // are being initialized. + // + // Internal for testing + internal Dictionary _deferredUpdates; + public override void Initialize(ProjectSnapshotManagerBase projectManager) { _projectManager = projectManager; _projectManager.Workspace.WorkspaceChanged += Workspace_WorkspaceChanged; + _deferredUpdates = new Dictionary(); + InitializeSolution(_projectManager.Workspace.CurrentSolution); } @@ -47,10 +59,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem case WorkspaceChangeKind.ProjectChanged: case WorkspaceChangeKind.ProjectReloaded: { - project = e.NewSolution.GetProject(e.ProjectId); - Debug.Assert(project != null); - - _projectManager.WorkspaceProjectChanged(project); + EnqueueUpdate(e.ProjectId); break; } @@ -81,5 +90,27 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem break; } } + + private void EnqueueUpdate(ProjectId projectId) + { + // A race is not possible here because we use the main thread to synchronize the updates + // by capturing the sync context. + if (!_deferredUpdates.TryGetValue(projectId, out var update) || update.IsCompleted) + { + _deferredUpdates[projectId] = UpdateAfterDelay(projectId); + } + } + + private async Task UpdateAfterDelay(ProjectId projectId) + { + await Task.Delay(ProjectChangeDelay); + + var solution = _projectManager.Workspace.CurrentSolution; + var workspaceProject = solution.GetProject(projectId); + if (workspaceProject != null) + { + _projectManager.WorkspaceProjectChanged(workspaceProject); + } + } } } diff --git a/src/Microsoft.CodeAnalysis.Razor/Properties/AssemblyInfo.cs b/src/Microsoft.CodeAnalysis.Razor/Properties/AssemblyInfo.cs index 52df72dac7..768c23b6a9 100644 --- a/src/Microsoft.CodeAnalysis.Razor/Properties/AssemblyInfo.cs +++ b/src/Microsoft.CodeAnalysis.Razor/Properties/AssemblyInfo.cs @@ -9,6 +9,7 @@ using System.Runtime.CompilerServices; [assembly: InternalsVisibleTo("Microsoft.CodeAnalysis.Razor.Workspaces, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] [assembly: InternalsVisibleTo("Microsoft.CodeAnalysis.Razor.Workspaces.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] [assembly: InternalsVisibleTo("Microsoft.CodeAnalysis.Remote.Razor, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] +[assembly: InternalsVisibleTo("Microsoft.VisualStudio.Editor.Razor, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] [assembly: InternalsVisibleTo("Microsoft.VisualStudio.LanguageServices.Razor, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] [assembly: InternalsVisibleTo("Microsoft.VisualStudio.RazorExtension, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")] diff --git a/src/Microsoft.VisualStudio.Editor.Razor/DefaultVisualStudioDocumentTracker.cs b/src/Microsoft.VisualStudio.Editor.Razor/DefaultVisualStudioDocumentTracker.cs index b9c976bdb9..02311c217b 100644 --- a/src/Microsoft.VisualStudio.Editor.Razor/DefaultVisualStudioDocumentTracker.cs +++ b/src/Microsoft.VisualStudio.Editor.Razor/DefaultVisualStudioDocumentTracker.cs @@ -292,7 +292,9 @@ namespace Microsoft.VisualStudio.Editor.Razor switch (e.Kind) { - case ProjectChangeKind.DocumentsChanged: + case ProjectChangeKind.DocumentAdded: + case ProjectChangeKind.DocumentRemoved: + case ProjectChangeKind.DocumentChanged: // Nothing to do. break; @@ -311,11 +313,6 @@ namespace Microsoft.VisualStudio.Editor.Razor OnContextChanged(ContextChangeKind.ProjectChanged); break; - case ProjectChangeKind.DocumentContentChanged: - - // Do nothing - break; - default: throw new InvalidOperationException($"Unknown ProjectChangeKind {e.Kind}"); } diff --git a/src/Microsoft.VisualStudio.Editor.Razor/Documents/EditorDocument.cs b/src/Microsoft.VisualStudio.Editor.Razor/Documents/EditorDocument.cs new file mode 100644 index 0000000000..b048defba7 --- /dev/null +++ b/src/Microsoft.VisualStudio.Editor.Razor/Documents/EditorDocument.cs @@ -0,0 +1,162 @@ +// 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; +using Microsoft.CodeAnalysis.Text; +using Microsoft.VisualStudio.Text; + +namespace Microsoft.VisualStudio.Editor.Razor.Documents +{ + // Tracks the mutable state associated with a document - in contrast to DocumentSnapshot + // which tracks the state at a point in time. + internal sealed class EditorDocument : IDisposable + { + private readonly EditorDocumentManager _documentManager; + private readonly FileChangeTracker _fileTracker; + private readonly SnapshotChangeTracker _snapshotTracker; + private readonly EventHandler _changedOnDisk; + private readonly EventHandler _changedInEditor; + private readonly EventHandler _opened; + private readonly EventHandler _closed; + + private bool _disposed; + + public EditorDocument( + EditorDocumentManager documentManager, + string projectFilePath, + string documentFilePath, + TextLoader textLoader, + FileChangeTracker fileTracker, + ITextBuffer textBuffer, + EventHandler changedOnDisk, + EventHandler changedInEditor, + EventHandler opened, + EventHandler closed) + { + if (documentManager == null) + { + throw new ArgumentNullException(nameof(documentManager)); + } + + if (projectFilePath == null) + { + throw new ArgumentNullException(nameof(projectFilePath)); + } + + if (documentFilePath == null) + { + throw new ArgumentNullException(nameof(documentFilePath)); + } + + if (textLoader == null) + { + throw new ArgumentNullException(nameof(textLoader)); + } + + if (fileTracker == null) + { + throw new ArgumentNullException(nameof(fileTracker)); + } + + _documentManager = documentManager; + ProjectFilePath = projectFilePath; + DocumentFilePath = documentFilePath; + TextLoader = textLoader; + _fileTracker = fileTracker; + _changedOnDisk = changedOnDisk; + _changedInEditor = changedInEditor; + _opened = opened; + _closed = closed; + + _snapshotTracker = new SnapshotChangeTracker(); + _fileTracker.Changed += ChangeTracker_Changed; + + // Only one of these should be active at a time. + if (textBuffer == null) + { + _fileTracker.StartListening(); + } + else + { + _snapshotTracker.StartTracking(textBuffer); + + EditorTextBuffer = textBuffer; + EditorTextContainer = textBuffer.AsTextContainer(); + EditorTextContainer.TextChanged += TextContainer_Changed; + } + } + + public string ProjectFilePath { get; } + + public string DocumentFilePath { get; } + + public bool IsOpenInEditor => EditorTextBuffer != null; + + public SourceTextContainer EditorTextContainer { get; private set; } + + public ITextBuffer EditorTextBuffer { get; private set; } + + public TextLoader TextLoader { get; } + + public void ProcessOpen(ITextBuffer textBuffer) + { + if (textBuffer == null) + { + throw new ArgumentNullException(nameof(textBuffer)); + } + + _fileTracker.StopListening(); + + _snapshotTracker.StartTracking(textBuffer); + EditorTextBuffer = textBuffer; + EditorTextContainer = textBuffer.AsTextContainer(); + EditorTextContainer.TextChanged += TextContainer_Changed; + + _opened?.Invoke(this, EventArgs.Empty); + } + + public void ProcessClose() + { + _closed?.Invoke(this, EventArgs.Empty); + + _snapshotTracker.StopTracking(EditorTextBuffer); + + EditorTextContainer.TextChanged -= TextContainer_Changed; + EditorTextContainer = null; + EditorTextBuffer = null; + + _fileTracker.StartListening(); + } + + private void ChangeTracker_Changed(object sender, FileChangeEventArgs e) + { + if (e.Kind == FileChangeKind.Changed) + { + _changedOnDisk?.Invoke(this, EventArgs.Empty); + } + } + + private void TextContainer_Changed(object sender, TextChangeEventArgs e) + { + _changedInEditor?.Invoke(this, EventArgs.Empty); + } + + public void Dispose() + { + if (!_disposed) + { + _fileTracker.Changed -= ChangeTracker_Changed; + _fileTracker.StopListening(); + + EditorTextContainer.TextChanged -= TextContainer_Changed; + EditorTextContainer = null; + EditorTextBuffer = null; + + _documentManager.RemoveDocument(this); + + _disposed = true; + } + } + } +} diff --git a/src/Microsoft.VisualStudio.Editor.Razor/Documents/EditorDocumentManager.cs b/src/Microsoft.VisualStudio.Editor.Razor/Documents/EditorDocumentManager.cs new file mode 100644 index 0000000000..89f8745e6c --- /dev/null +++ b/src/Microsoft.VisualStudio.Editor.Razor/Documents/EditorDocumentManager.cs @@ -0,0 +1,25 @@ +// 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; +using Microsoft.CodeAnalysis.Razor; + +namespace Microsoft.VisualStudio.Editor.Razor.Documents +{ + internal abstract class EditorDocumentManager : IWorkspaceService + { + public abstract EditorDocument GetOrCreateDocument( + DocumentKey key, + EventHandler changedOnDisk, + EventHandler changedInEditor, + EventHandler opened, + EventHandler closed); + + public abstract bool TryGetDocument(DocumentKey key, out EditorDocument document); + + public abstract bool TryGetMatchingDocuments(string filePath, out EditorDocument[] documents); + + public abstract void RemoveDocument(EditorDocument document); + } +} diff --git a/src/Microsoft.VisualStudio.Editor.Razor/Documents/EditorDocumentManagerBase.cs b/src/Microsoft.VisualStudio.Editor.Razor/Documents/EditorDocumentManagerBase.cs new file mode 100644 index 0000000000..34abfe79d2 --- /dev/null +++ b/src/Microsoft.VisualStudio.Editor.Razor/Documents/EditorDocumentManagerBase.cs @@ -0,0 +1,221 @@ +// 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; +using Microsoft.CodeAnalysis.Razor; +using Microsoft.VisualStudio.Text; + +namespace Microsoft.VisualStudio.Editor.Razor.Documents +{ + // Similar to the DocumentProvider in dotnet/Roslyn - but simplified quite a bit to remove + // concepts that we don't need. Responsible for providing data about text changes for documents + // and editor open/closed state. + internal abstract class EditorDocumentManagerBase : EditorDocumentManager + { + private readonly FileChangeTrackerFactory _fileChangeTrackerFactory; + private readonly ForegroundDispatcher _foregroundDispatcher; + + private readonly Dictionary _documents; + private readonly Dictionary> _documentsByFilePath; + protected readonly object _lock; + + public EditorDocumentManagerBase( + ForegroundDispatcher foregroundDispatcher, + FileChangeTrackerFactory fileChangeTrackerFactory) + { + if (foregroundDispatcher == null) + { + throw new ArgumentNullException(nameof(foregroundDispatcher)); + } + + if (fileChangeTrackerFactory == null) + { + throw new ArgumentNullException(nameof(fileChangeTrackerFactory)); + } + + _foregroundDispatcher = foregroundDispatcher; + _fileChangeTrackerFactory = fileChangeTrackerFactory; + + _documents = new Dictionary(); + _documentsByFilePath = new Dictionary>(FilePathComparer.Instance); + _lock = new object(); + } + + protected ForegroundDispatcher ForegroundDispatcher => _foregroundDispatcher; + + protected abstract ITextBuffer GetTextBufferForOpenDocument(string filePath); + + protected abstract void OnDocumentOpened(EditorDocument document); + + protected abstract void OnDocumentClosed(EditorDocument document); + + public sealed override bool TryGetDocument(DocumentKey key, out EditorDocument document) + { + _foregroundDispatcher.AssertForegroundThread(); + + lock (_lock) + { + return _documents.TryGetValue(key, out document); + } + } + + public sealed override bool TryGetMatchingDocuments(string filePath, out EditorDocument[] documents) + { + _foregroundDispatcher.AssertForegroundThread(); + + lock (_lock) + { + if (!_documentsByFilePath.TryGetValue(filePath, out var keys)) + { + documents = null; + return false; + } + + documents = new EditorDocument[keys.Count]; + for (var i = 0; i < keys.Count; i++) + { + documents[i] = _documents[keys[i]]; + } + + return true; + } + } + + public sealed override EditorDocument GetOrCreateDocument( + DocumentKey key, + EventHandler changedOnDisk, + EventHandler changedInEditor, + EventHandler opened, + EventHandler closed) + { + _foregroundDispatcher.AssertForegroundThread(); + + EditorDocument document; + + lock (_lock) + { + if (TryGetDocument(key, out document)) + { + return document; + } + + // Check if the document is already open and initialized, and associate a buffer if possible. + var textBuffer = GetTextBufferForOpenDocument(key.DocumentFilePath); + document = new EditorDocument( + this, + key.ProjectFilePath, + key.DocumentFilePath, + new FileTextLoader(key.DocumentFilePath, defaultEncoding: null), + _fileChangeTrackerFactory.Create(key.DocumentFilePath), + textBuffer, + changedOnDisk, + changedInEditor, + opened, + closed); + + _documents.Add(key, document); + + if (!_documentsByFilePath.TryGetValue(key.DocumentFilePath, out var documents)) + { + documents = new List(); + _documentsByFilePath.Add(key.DocumentFilePath, documents); + } + + if (!documents.Contains(key)) + { + documents.Add(key); + } + + if (document.IsOpenInEditor) + { + OnDocumentOpened(document); + } + + return document; + } + } + + protected void DocumentOpened(string filePath, ITextBuffer textBuffer) + { + if (filePath == null) + { + throw new ArgumentNullException(nameof(filePath)); + } + + if (textBuffer == null) + { + throw new ArgumentNullException(nameof(textBuffer)); + } + + _foregroundDispatcher.AssertForegroundThread(); + + lock (_lock) + { + if (TryGetMatchingDocuments(filePath, out var documents)) + { + for (var i = 0; i < documents.Length; i++) + { + var document = documents[i]; + + document.ProcessOpen(textBuffer); + OnDocumentOpened(document); + } + } + } + } + + protected void DocumentClosed(string filePath) + { + if (filePath == null) + { + throw new ArgumentNullException(nameof(filePath)); + } + + _foregroundDispatcher.AssertForegroundThread(); + + lock (_lock) + { + if (TryGetMatchingDocuments(filePath, out var documents)) + { + for (var i = 0; i < documents.Length; i++) + { + var document = documents[i]; + + document.ProcessClose(); + OnDocumentClosed(document); + } + } + } + } + + public sealed override void RemoveDocument(EditorDocument document) + { + if (document == null) + { + throw new ArgumentNullException(nameof(document)); + } + + _foregroundDispatcher.AssertForegroundThread(); + + var key = new DocumentKey(document.ProjectFilePath, document.DocumentFilePath); + if (_documentsByFilePath.TryGetValue(document.DocumentFilePath, out var documents)) + { + documents.Remove(key); + + if (documents.Count == 0) + { + _documentsByFilePath.Remove(document.DocumentFilePath); + } + } + + _documents.Remove(key); + + if (document.IsOpenInEditor) + { + OnDocumentClosed(document); + } + } + } +} diff --git a/src/Microsoft.VisualStudio.Editor.Razor/Documents/EditorDocumentManagerListener.cs b/src/Microsoft.VisualStudio.Editor.Razor/Documents/EditorDocumentManagerListener.cs new file mode 100644 index 0000000000..33abac5804 --- /dev/null +++ b/src/Microsoft.VisualStudio.Editor.Razor/Documents/EditorDocumentManagerListener.cs @@ -0,0 +1,100 @@ +// 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.Razor; +using Microsoft.CodeAnalysis.Razor.ProjectSystem; + +namespace Microsoft.VisualStudio.Editor.Razor.Documents +{ + // Hooks up the document manager to project snapshot events. The project snapshot manager + // tracks the existance of projects/files and the the document manager watches for changes. + // + // This class forwards notifications in both directions. + [Export(typeof(ProjectSnapshotChangeTrigger))] + internal class EditorDocumentManagerListener : ProjectSnapshotChangeTrigger + { + private readonly EventHandler _onChangedOnDisk; + private readonly EventHandler _onChangedInEditor; + private readonly EventHandler _onOpened; + private readonly EventHandler _onClosed; + + private EditorDocumentManager _documentManager; + private ProjectSnapshotManagerBase _projectManager; + + [ImportingConstructor] + public EditorDocumentManagerListener() + { + _onChangedOnDisk = Document_ChangedOnDisk; + _onChangedInEditor = Document_ChangedInEditor; + _onOpened = Document_Opened; + _onClosed = Document_Closed; + } + + public override void Initialize(ProjectSnapshotManagerBase projectManager) + { + if (projectManager == null) + { + throw new ArgumentNullException(nameof(projectManager)); + } + + _projectManager = projectManager; + _documentManager = projectManager.Workspace.Services.GetRequiredService(); + + _projectManager.Changed += ProjectManager_Changed; + } + + private void ProjectManager_Changed(object sender, ProjectChangeEventArgs e) + { + switch (e.Kind) + { + case ProjectChangeKind.DocumentAdded: + { + var key = new DocumentKey(e.ProjectFilePath, e.DocumentFilePath); + var document = _documentManager.GetOrCreateDocument(key, _onChangedOnDisk, _onChangedOnDisk, _onOpened, _onClosed); + if (document.IsOpenInEditor) + { + Document_Opened(document, EventArgs.Empty); + } + + break; + } + + case ProjectChangeKind.DocumentRemoved: + { + // This class 'owns' the document entry so it's safe for us to dispose it. + if (_documentManager.TryGetDocument(new DocumentKey(e.ProjectFilePath, e.DocumentFilePath), out var document)) + { + document.Dispose(); + } + break; + } + } + } + + private void Document_ChangedOnDisk(object sender, EventArgs e) + { + var document = (EditorDocument)sender; + _projectManager.DocumentChanged(document.ProjectFilePath, document.DocumentFilePath, document.TextLoader); + } + + private void Document_ChangedInEditor(object sender, EventArgs e) + { + var document = (EditorDocument)sender; + _projectManager.DocumentChanged(document.ProjectFilePath, document.DocumentFilePath, document.EditorTextContainer.CurrentText); + } + + private void Document_Opened(object sender, EventArgs e) + { + var document = (EditorDocument)sender; + _projectManager.DocumentOpened(document.ProjectFilePath, document.DocumentFilePath, document.EditorTextContainer.CurrentText); + } + + private void Document_Closed(object sender, EventArgs e) + { + var document = (EditorDocument)sender; + _projectManager.DocumentClosed(document.ProjectFilePath, document.DocumentFilePath, document.TextLoader); + } + } +} diff --git a/src/Microsoft.VisualStudio.Editor.Razor/Documents/SnapshotChangeTracker.cs b/src/Microsoft.VisualStudio.Editor.Razor/Documents/SnapshotChangeTracker.cs new file mode 100644 index 0000000000..13a75d5ea7 --- /dev/null +++ b/src/Microsoft.VisualStudio.Editor.Razor/Documents/SnapshotChangeTracker.cs @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.VisualStudio.Text; + +namespace Microsoft.VisualStudio.Editor.Razor.Documents +{ + // See ReiteratedVersionSnapshotTracker in dotnet/Roslyn -- this is primarily here for the + // side-effect of making sure the last 'reiterated' snapshot is retained in memory. + // + // Since we're interacting with the workspace in the same way, we're doing the same thing. + internal class SnapshotChangeTracker + { + private ITextBuffer _textBuffer; + private ITextSnapshot _snapshot; + + public void StartTracking(ITextBuffer buffer) + { + // buffer has changed. stop tracking old buffer + if (_textBuffer != null && buffer != _textBuffer) + { + _textBuffer.ChangedHighPriority -= OnTextBufferChanged; + + _textBuffer = null; + _snapshot = null; + } + + // start tracking new buffer + if (buffer != null && _snapshot == null) + { + _snapshot = buffer.CurrentSnapshot; + _textBuffer = buffer; + + buffer.ChangedHighPriority += OnTextBufferChanged; + } + } + + public void StopTracking(ITextBuffer buffer) + { + if (_textBuffer == buffer && buffer != null && _snapshot != null) + { + buffer.ChangedHighPriority -= OnTextBufferChanged; + + _textBuffer = null; + _snapshot = null; + } + } + + private void OnTextBufferChanged(object sender, TextContentChangedEventArgs e) + { + if (sender is ITextBuffer buffer) + { + var snapshot = _snapshot; + if (snapshot != null && snapshot.Version != null && e.AfterVersion != null && + snapshot.Version.ReiteratedVersionNumber < e.AfterVersion.ReiteratedVersionNumber) + { + _snapshot = e.After; + } + } + } + } +} diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/Documents/RunningDocumentTableEventSink.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/Documents/RunningDocumentTableEventSink.cs new file mode 100644 index 0000000000..76390ba81b --- /dev/null +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/Documents/RunningDocumentTableEventSink.cs @@ -0,0 +1,62 @@ +// 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.VisualStudio.Shell.Interop; + +namespace Microsoft.VisualStudio.Editor.Razor.Documents +{ + internal class RunningDocumentTableEventSink : IVsRunningDocTableEvents3 + { + private readonly VisualStudioEditorDocumentManager _documentManager; + + public RunningDocumentTableEventSink(VisualStudioEditorDocumentManager documentManager) + { + if (documentManager == null) + { + throw new ArgumentNullException(nameof(documentManager)); + } + + _documentManager = documentManager; + } + + public int OnAfterAttributeChangeEx(uint docCookie, uint grfAttribs, IVsHierarchy pHierOld, uint itemidOld, string pszMkDocumentOld, IVsHierarchy pHierNew, uint itemidNew, string pszMkDocumentNew) + { + // Document has been initialized. + if ((grfAttribs & (uint)__VSRDTATTRIB3.RDTA_DocumentInitialized) != 0) + { + _documentManager.DocumentOpened(docCookie); + } + + if ((grfAttribs & (uint)__VSRDTATTRIB.RDTA_MkDocument) != 0) + { + _documentManager.DocumentRenamed(docCookie, pszMkDocumentOld, pszMkDocumentNew); + } + + return VSConstants.S_OK; + } + + public int OnBeforeLastDocumentUnlock(uint docCookie, uint dwRDTLockType, uint dwReadLocksRemaining, uint dwEditLocksRemaining) + { + // Document is being closed + if (dwReadLocksRemaining + dwEditLocksRemaining == 0) + { + _documentManager.DocumentClosed(docCookie); + } + + return VSConstants.S_OK; + } + + public int OnBeforeSave(uint docCookie) => VSConstants.S_OK; + + public int OnAfterSave(uint docCookie) => VSConstants.S_OK; + + public int OnAfterAttributeChange(uint docCookie, uint grfAttribs) => VSConstants.S_OK; + + public int OnBeforeDocumentWindowShow(uint docCookie, int fFirstShow, IVsWindowFrame pFrame) => VSConstants.S_OK; + + public int OnAfterDocumentWindowHide(uint docCookie, IVsWindowFrame pFrame) => VSConstants.S_OK; + + public int OnAfterFirstDocumentLock(uint docCookie, uint dwRDTLockType, uint dwReadLocksRemaining, uint dwEditLocksRemaining) => VSConstants.S_OK; + } +} diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/Documents/VisualStudioEditorDocumentManager.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/Documents/VisualStudioEditorDocumentManager.cs new file mode 100644 index 0000000000..ab6397d772 --- /dev/null +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/Documents/VisualStudioEditorDocumentManager.cs @@ -0,0 +1,254 @@ +// 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.Runtime.InteropServices; +using Microsoft.CodeAnalysis.Razor; +using Microsoft.VisualStudio.Shell.Interop; +using Microsoft.VisualStudio.Text; +using Microsoft.VisualStudio.TextManager.Interop; + +namespace Microsoft.VisualStudio.Editor.Razor.Documents +{ + // Similar to the DocumentProvider in dotnet/Roslyn - but simplified quite a bit to remove + // concepts that we don't need. Responsible for providing data about text changes for documents + // and editor open/closed state. + internal class VisualStudioEditorDocumentManager : EditorDocumentManagerBase + { + private readonly IVsEditorAdaptersFactoryService _editorAdaptersFactory; + + private readonly IVsRunningDocumentTable4 _runningDocumentTable; + private readonly uint _rdtCookie; + + private readonly Dictionary> _documentsByCookie; + private readonly Dictionary _cookiesByDocument; + + public VisualStudioEditorDocumentManager( + ForegroundDispatcher foregroundDispatcher, + FileChangeTrackerFactory fileChangeTrackerFactory, + IVsRunningDocumentTable runningDocumentTable, + IVsEditorAdaptersFactoryService editorAdaptersFactory) + : base(foregroundDispatcher, fileChangeTrackerFactory) + { + if (runningDocumentTable == null) + { + throw new ArgumentNullException(nameof(runningDocumentTable)); + } + + if (editorAdaptersFactory == null) + { + throw new ArgumentNullException(nameof(editorAdaptersFactory)); + } + + if (foregroundDispatcher == null) + { + throw new ArgumentNullException(nameof(foregroundDispatcher)); + } + + if (fileChangeTrackerFactory == null) + { + throw new ArgumentNullException(nameof(fileChangeTrackerFactory)); + } + + _runningDocumentTable = (IVsRunningDocumentTable4)runningDocumentTable; + _editorAdaptersFactory = editorAdaptersFactory; + + var hr = runningDocumentTable.AdviseRunningDocTableEvents(new RunningDocumentTableEventSink(this), out _rdtCookie); + Marshal.ThrowExceptionForHR(hr); + + _documentsByCookie = new Dictionary>(); + _cookiesByDocument = new Dictionary(); + } + + protected override ITextBuffer GetTextBufferForOpenDocument(string filePath) + { + if (filePath == null) + { + throw new ArgumentNullException(nameof(filePath)); + } + + // Check if the document is already open and initialized, and associate a buffer if possible. + var cookie = VSConstants.VSCOOKIE_NIL; + ITextBuffer textBuffer = null; + if (_runningDocumentTable.IsMonikerValid(filePath) && + ((cookie = _runningDocumentTable.GetDocumentCookie(filePath)) != VSConstants.VSCOOKIE_NIL) && + (_runningDocumentTable.GetDocumentFlags(cookie) & (uint)_VSRDTFLAGS4.RDT_PendingInitialization) == 0) + { + var vsTextBuffer = ((object)_runningDocumentTable.GetDocumentData(cookie)) as VsTextBuffer; + textBuffer = vsTextBuffer == null ? null : _editorAdaptersFactory.GetDocumentBuffer(vsTextBuffer); + return textBuffer; + } + + return null; + } + + protected override void OnDocumentOpened(EditorDocument document) + { + var cookie = _runningDocumentTable.GetDocumentCookie(document.DocumentFilePath); + if (cookie != VSConstants.VSCOOKIE_NIL) + { + TrackOpenDocument(cookie, new DocumentKey(document.ProjectFilePath, document.DocumentFilePath)); + } + } + + protected override void OnDocumentClosed(EditorDocument document) + { + var key = new DocumentKey(document.ProjectFilePath, document.DocumentFilePath); + if (_cookiesByDocument.TryGetValue(key, out var cookie)) + { + UntrackOpenDocument(cookie, key); + } + } + + public void DocumentOpened(uint cookie) + { + ForegroundDispatcher.AssertForegroundThread(); + + lock (_lock) + { + // Casts avoid dynamic + if ((object)(_runningDocumentTable.GetDocumentData(cookie)) is IVsTextBuffer vsTextBuffer) + { + var filePath = _runningDocumentTable.GetDocumentMoniker(cookie); + if (!TryGetMatchingDocuments(filePath, out var documents)) + { + // This isn't a document that we're interesting in. + return; + } + + var textBuffer = _editorAdaptersFactory.GetDataBuffer(vsTextBuffer); + if (textBuffer == null) + { + // The text buffer has not been created yet, register to be notified when it is. + VsTextBufferDataEventsSink.Subscribe(vsTextBuffer, () => + { + BufferLoaded(vsTextBuffer, filePath); + }); + + return; + } + + // It's possible that events could be fired out of order and that this is a rename. + if (_documentsByCookie.ContainsKey(cookie)) + { + DocumentClosed(cookie, exceptFilePath: filePath); + } + + BufferLoaded(textBuffer, filePath, documents); + } + } + } + + public void BufferLoaded(IVsTextBuffer vsTextBuffer, string filePath) + { + ForegroundDispatcher.AssertForegroundThread(); + + var textBuffer = _editorAdaptersFactory.GetDocumentBuffer(vsTextBuffer); + if (textBuffer != null) + { + // We potentially waited for the editor to initialize on this code path, so requery + // the documents. + if (TryGetMatchingDocuments(filePath, out var documents)) + { + BufferLoaded(textBuffer, filePath, documents); + } + } + } + + public void BufferLoaded(ITextBuffer textBuffer, string filePath, EditorDocument[] documents) + { + ForegroundDispatcher.AssertForegroundThread(); + + lock (_lock) + { + for (var i = 0; i < documents.Length; i++) + { + DocumentOpened(filePath, textBuffer); + } + } + } + + public void DocumentClosed(uint cookie, string exceptFilePath = null) + { + ForegroundDispatcher.AssertForegroundThread(); + + lock (_lock) + { + if (!_documentsByCookie.TryGetValue(cookie, out var documents)) + { + return; + } + + // We have to deal with some complications here due to renames and event ordering and such. + // We we might see multiple documents open for a cookie (due to linked files), but only one of them + // has been renamed. In that case, we just process the change that we know about. + var filePaths = new HashSet(documents.Select(d => d.DocumentFilePath)); + filePaths.Remove(exceptFilePath); + + foreach (var filePath in filePaths) + { + DocumentClosed(filePath); + } + } + } + + public void DocumentRenamed(uint cookie, string fromFilePath, string toFilePath) + { + ForegroundDispatcher.AssertForegroundThread(); + + // Ignore changes is casing + if (FilePathComparer.Instance.Equals(fromFilePath, toFilePath)) + { + return; + } + + lock (_lock) + { + // Treat a rename as a close + reopen. + // + // Due to ordering issues, we could see a partial rename. This is why we need to pass the new + // file path here. + DocumentClosed(cookie, exceptFilePath: toFilePath); + } + + // Try to open any existing documents that match the new name. + if ((_runningDocumentTable.GetDocumentFlags(cookie) & (uint)_VSRDTFLAGS4.RDT_PendingInitialization) == 0) + { + DocumentOpened(cookie); + } + } + + private void TrackOpenDocument(uint cookie, DocumentKey key) + { + if (!_documentsByCookie.TryGetValue(cookie, out var documents)) + { + documents = new List(); + _documentsByCookie.Add(cookie, documents); + } + + if (!documents.Contains(key)) + { + documents.Add(key); + } + + _cookiesByDocument[key] = cookie; + } + + private void UntrackOpenDocument(uint cookie, DocumentKey key) + { + if (_documentsByCookie.TryGetValue(cookie, out var documents)) + { + documents.Remove(key); + + if (documents.Count == 0) + { + _documentsByCookie.Remove(cookie); + } + } + + _cookiesByDocument.Remove(key); + } + } +} diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/Documents/VisualStudioEditorDocumentManagerFactory.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/Documents/VisualStudioEditorDocumentManagerFactory.cs new file mode 100644 index 0000000000..1fd0f508d3 --- /dev/null +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/Documents/VisualStudioEditorDocumentManagerFactory.cs @@ -0,0 +1,60 @@ +// 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; +using Microsoft.CodeAnalysis.Razor; +using Microsoft.VisualStudio.Shell; +using Microsoft.VisualStudio.Shell.Interop; + +namespace Microsoft.VisualStudio.Editor.Razor.Documents +{ + [Shared] + [ExportWorkspaceServiceFactory(typeof(EditorDocumentManager), ServiceLayer.Host)] + internal class VisualStudioEditorDocumentManagerFactory : IWorkspaceServiceFactory + { + private readonly SVsServiceProvider _serviceProvider; + private readonly IVsEditorAdaptersFactoryService _editorAdaptersFactory; + private readonly ForegroundDispatcher _foregroundDispatcher; + + [ImportingConstructor] + public VisualStudioEditorDocumentManagerFactory( + SVsServiceProvider serviceProvider, + IVsEditorAdaptersFactoryService editorAdaptersFactory, + ForegroundDispatcher foregroundDispatcher) + { + if (serviceProvider == null) + { + throw new ArgumentNullException(nameof(serviceProvider)); + } + + if (editorAdaptersFactory == null) + { + throw new ArgumentNullException(nameof(editorAdaptersFactory)); + } + + if (foregroundDispatcher == null) + { + throw new ArgumentNullException(nameof(foregroundDispatcher)); + } + + _serviceProvider = serviceProvider; + _editorAdaptersFactory = editorAdaptersFactory; + _foregroundDispatcher = foregroundDispatcher; + } + + public IWorkspaceService CreateService(HostWorkspaceServices workspaceServices) + { + if (workspaceServices == null) + { + throw new ArgumentNullException(nameof(workspaceServices)); + } + + var runningDocumentTable = (IVsRunningDocumentTable)_serviceProvider.GetService(typeof(SVsRunningDocumentTable)); + var fileChangeTrackerFactory = workspaceServices.GetRequiredService(); + return new VisualStudioEditorDocumentManager(_foregroundDispatcher, fileChangeTrackerFactory, runningDocumentTable, _editorAdaptersFactory); + } + } +} diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/Documents/VsTextBufferDataEventsSink.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/Documents/VsTextBufferDataEventsSink.cs new file mode 100644 index 0000000000..c629f929d5 --- /dev/null +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/Documents/VsTextBufferDataEventsSink.cs @@ -0,0 +1,56 @@ +// 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.VisualStudio.OLE.Interop; +using Microsoft.VisualStudio.TextManager.Interop; + +namespace Microsoft.VisualStudio.Editor.Razor.Documents +{ + internal class VsTextBufferDataEventsSink : IVsTextBufferDataEvents + { + private readonly Action _action; + private readonly IConnectionPoint _connectionPoint; + private uint _cookie; + + public static void Subscribe(IVsTextBuffer vsTextBuffer, Action action) + { + if (vsTextBuffer == null) + { + throw new ArgumentNullException(nameof(vsTextBuffer)); + } + + if (action == null) + { + throw new ArgumentNullException(nameof(action)); + } + + var connectionPointContainer = (IConnectionPointContainer)vsTextBuffer; + + var guid = typeof(IVsTextBufferDataEvents).GUID; + connectionPointContainer.FindConnectionPoint(ref guid, out var connectionPoint); + + var sink = new VsTextBufferDataEventsSink(connectionPoint, action); + connectionPoint.Advise(sink, out sink._cookie); + } + + private VsTextBufferDataEventsSink(IConnectionPoint connectionPoint, Action action) + { + _connectionPoint = connectionPoint; + _action = action; + } + + public void OnFileChanged(uint grfChange, uint dwFileAttrs) + { + // ignore + } + + public int OnLoadCompleted(int fReload) + { + _connectionPoint.Unadvise(_cookie); + _action(); + + return VSConstants.S_OK; + } + } +} 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 26d1ac3025..32296b51c6 100644 --- a/src/Microsoft.VisualStudio.LanguageServices.Razor/Microsoft.VisualStudio.LanguageServices.Razor.csproj +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/Microsoft.VisualStudio.LanguageServices.Razor.csproj @@ -40,10 +40,19 @@ - + + + + + + + false + + + - - + @@ -268,7 +271,6 @@ - @@ -276,15 +278,12 @@ MSBuild Microsoft\VisualStudio\Razor\ - true MSBuild Microsoft\VisualStudio\Razor\Rules\ - - - diff --git a/tooling/Microsoft.VisualStudio.RazorExtension/RazorInfo/DirectiveCollectionViewModel.cs b/tooling/Microsoft.VisualStudio.RazorExtension/RazorInfo/DirectiveCollectionViewModel.cs new file mode 100644 index 0000000000..ea04f1512b --- /dev/null +++ b/tooling/Microsoft.VisualStudio.RazorExtension/RazorInfo/DirectiveCollectionViewModel.cs @@ -0,0 +1,35 @@ +// 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.ObjectModel; +using System.Linq; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.CodeAnalysis.Razor.ProjectSystem; + +namespace Microsoft.VisualStudio.RazorExtension.RazorInfo +{ + public class DirectiveCollectionViewModel : NotifyPropertyChanged + { + private readonly ProjectSnapshot _project; + + internal DirectiveCollectionViewModel(ProjectSnapshot project) + { + _project = project; + + Directives = new ObservableCollection(); + + var feature = _project.GetProjectEngine().EngineFeatures.OfType().FirstOrDefault(); + foreach (var directive in feature?.Directives ?? Array.Empty()) + { + Directives.Add(new DirectiveItemViewModel(directive)); + } + } + + public ObservableCollection Directives { get; } + } +} + +#endif diff --git a/tooling/Microsoft.VisualStudio.RazorExtension/RazorInfo/DirectiveDescriptorViewModel.cs b/tooling/Microsoft.VisualStudio.RazorExtension/RazorInfo/DirectiveItemViewModel.cs similarity index 87% rename from tooling/Microsoft.VisualStudio.RazorExtension/RazorInfo/DirectiveDescriptorViewModel.cs rename to tooling/Microsoft.VisualStudio.RazorExtension/RazorInfo/DirectiveItemViewModel.cs index d37ec05168..7dcd0a7fbb 100644 --- a/tooling/Microsoft.VisualStudio.RazorExtension/RazorInfo/DirectiveDescriptorViewModel.cs +++ b/tooling/Microsoft.VisualStudio.RazorExtension/RazorInfo/DirectiveItemViewModel.cs @@ -7,11 +7,11 @@ using Microsoft.AspNetCore.Razor.Language; namespace Microsoft.VisualStudio.RazorExtension.RazorInfo { - public class DirectiveDescriptorViewModel : NotifyPropertyChanged + public class DirectiveItemViewModel : NotifyPropertyChanged { private readonly DirectiveDescriptor _directive; - internal DirectiveDescriptorViewModel(DirectiveDescriptor directive) + internal DirectiveItemViewModel(DirectiveDescriptor directive) { _directive = directive; diff --git a/tooling/Microsoft.VisualStudio.RazorExtension/RazorInfo/DocumentCollectionViewModel.cs b/tooling/Microsoft.VisualStudio.RazorExtension/RazorInfo/DocumentCollectionViewModel.cs new file mode 100644 index 0000000000..b384dae501 --- /dev/null +++ b/tooling/Microsoft.VisualStudio.RazorExtension/RazorInfo/DocumentCollectionViewModel.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. + +#if RAZOR_EXTENSION_DEVELOPER_MODE + +using System; +using System.Collections.ObjectModel; +using Microsoft.CodeAnalysis.Razor.ProjectSystem; + +namespace Microsoft.VisualStudio.RazorExtension.RazorInfo +{ + public class DocumentCollectionViewModel : NotifyPropertyChanged + { + private readonly ProjectSnapshotManager _projectManager; + private readonly Action _errorHandler; + + private ProjectSnapshot _project; + + internal DocumentCollectionViewModel(ProjectSnapshotManager projectManager, ProjectSnapshot project, Action errorHandler) + { + _projectManager = projectManager; + _project = project; + _errorHandler = errorHandler; + + Documents = new ObservableCollection(); + + foreach (var filePath in project.DocumentFilePaths) + { + Documents.Add(new DocumentItemViewModel(projectManager, project.GetDocument(filePath), _errorHandler)); + } + } + + public ObservableCollection Documents { get; } + + internal void OnChange(ProjectChangeEventArgs e) + { + switch (e.Kind) + { + case ProjectChangeKind.DocumentAdded: + { + _project = _projectManager.GetLoadedProject(e.ProjectFilePath); + Documents.Add(new DocumentItemViewModel(_projectManager, _project.GetDocument(e.DocumentFilePath), _errorHandler)); + break; + } + + case ProjectChangeKind.DocumentRemoved: + { + _project = _projectManager.GetLoadedProject(e.ProjectFilePath); + + for (var i = Documents.Count - 1; i >= 0; i--) + { + if (Documents[i].FilePath == e.DocumentFilePath) + { + Documents.RemoveAt(i); + break; + } + } + + break; + } + + case ProjectChangeKind.DocumentChanged: + { + _project = _projectManager.GetLoadedProject(e.ProjectFilePath); + for (var i = Documents.Count - 1; i >= 0; i--) + { + if (Documents[i].FilePath == e.DocumentFilePath) + { + Documents[i] = new DocumentItemViewModel(_projectManager, _project.GetDocument(e.DocumentFilePath), _errorHandler); + break; + } + } + + break; + } + } + } + } +} + +#endif diff --git a/tooling/Microsoft.VisualStudio.RazorExtension/RazorInfo/DocumentItemViewModel.cs b/tooling/Microsoft.VisualStudio.RazorExtension/RazorInfo/DocumentItemViewModel.cs new file mode 100644 index 0000000000..6bcf750c85 --- /dev/null +++ b/tooling/Microsoft.VisualStudio.RazorExtension/RazorInfo/DocumentItemViewModel.cs @@ -0,0 +1,70 @@ +// 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.Threading.Tasks; +using System.Windows; +using Microsoft.CodeAnalysis.Razor.ProjectSystem; + +namespace Microsoft.VisualStudio.RazorExtension.RazorInfo +{ + public class DocumentItemViewModel : NotifyPropertyChanged + { + private readonly ProjectSnapshotManager _snapshotManager; + private readonly DocumentSnapshot _document; + private readonly Action _errorHandler; + + private Visibility _progressVisibility; + + internal DocumentItemViewModel(ProjectSnapshotManager snapshotManager, DocumentSnapshot document, Action errorHandler) + { + _snapshotManager = snapshotManager; + _document = document; + _errorHandler = errorHandler; + + InitializeGeneratedDocument(); + } + + public string FilePath => _document.FilePath; + + public string StatusText => _snapshotManager.IsDocumentOpen(_document.FilePath) ? "Open" : "Closed"; + + public string TargetPath => _document.TargetPath; + + public Visibility ProgressVisibility + { + get => _progressVisibility; + set + { + _progressVisibility = value; + OnPropertyChanged(); + } + } + + private async void InitializeGeneratedDocument() + { + ProgressVisibility = Visibility.Hidden; + + try + { + if (!_document.TryGetGeneratedOutput(out var result)) + { + ProgressVisibility = Visibility.Visible; + await _document.GetGeneratedOutputAsync(); + await Task.Delay(250); // Force a delay for the UI + } + } + catch (Exception ex) + { + _errorHandler(ex); + } + finally + { + ProgressVisibility = Visibility.Hidden; + } + } + } +} +#endif \ No newline at end of file diff --git a/tooling/Microsoft.VisualStudio.RazorExtension/RazorInfo/DocumentSnapshotViewModel.cs b/tooling/Microsoft.VisualStudio.RazorExtension/RazorInfo/DocumentSnapshotViewModel.cs deleted file mode 100644 index 29aafc8cdb..0000000000 --- a/tooling/Microsoft.VisualStudio.RazorExtension/RazorInfo/DocumentSnapshotViewModel.cs +++ /dev/null @@ -1,50 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -#if RAZOR_EXTENSION_DEVELOPER_MODE - -using Microsoft.CodeAnalysis.Razor.ProjectSystem; - -namespace Microsoft.VisualStudio.RazorExtension.RazorInfo -{ - public class DocumentSnapshotViewModel : NotifyPropertyChanged - { - private double _progress; - - internal DocumentSnapshotViewModel(DocumentSnapshot document) - { - Document = document; - - InitializeGeneratedDocument(); - } - - internal DocumentSnapshot Document { get; } - - public string FilePath => Document.FilePath; - - public string TargetPath => Document.TargetPath; - - public bool CodeGenerationInProgress => _progress < 100; - - public double CodeGenerationProgress => _progress; - - private async void InitializeGeneratedDocument() - { - _progress = 0; - OnPropertyChanged(nameof(CodeGenerationInProgress)); - OnPropertyChanged(nameof(CodeGenerationProgress)); - - try - { - await Document.GetGeneratedOutputAsync(); - } - finally - { - _progress = 100; - OnPropertyChanged(nameof(CodeGenerationInProgress)); - OnPropertyChanged(nameof(CodeGenerationProgress)); - } - } - } -} -#endif \ No newline at end of file diff --git a/tooling/Microsoft.VisualStudio.RazorExtension/RazorInfo/NullToEnabledConverter.cs b/tooling/Microsoft.VisualStudio.RazorExtension/RazorInfo/NullToEnabledConverter.cs new file mode 100644 index 0000000000..89d7f6263d --- /dev/null +++ b/tooling/Microsoft.VisualStudio.RazorExtension/RazorInfo/NullToEnabledConverter.cs @@ -0,0 +1,30 @@ +// 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.Globalization; +using System.Windows.Data; + +namespace Microsoft.VisualStudio.RazorExtension.RazorInfo +{ + public class NullToEnabledConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (targetType == typeof(bool)) + { + return value != null; + } + + return null; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + return null; + } + } +} +#endif diff --git a/tooling/Microsoft.VisualStudio.RazorExtension/RazorInfo/ProjectInfoViewModel.cs b/tooling/Microsoft.VisualStudio.RazorExtension/RazorInfo/ProjectInfoViewModel.cs deleted file mode 100644 index 8cfa8451a6..0000000000 --- a/tooling/Microsoft.VisualStudio.RazorExtension/RazorInfo/ProjectInfoViewModel.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. - -#if RAZOR_EXTENSION_DEVELOPER_MODE -using System.Collections.ObjectModel; -using System.Windows; - -namespace Microsoft.VisualStudio.RazorExtension.RazorInfo -{ - public class ProjectInfoViewModel : NotifyPropertyChanged - { - private ObservableCollection _directives; - private ObservableCollection _documents; - private ObservableCollection _tagHelpers; - private bool _tagHelpersLoading; - - public ObservableCollection Directives - { - get { return _directives; } - set - { - _directives = value; - OnPropertyChanged(); - } - } - - public ObservableCollection Documents - { - get { return _documents; } - set - { - _documents = value; - OnPropertyChanged(); - } - } - - public ObservableCollection TagHelpers - { - get { return _tagHelpers; } - set - { - _tagHelpers = value; - OnPropertyChanged(); - } - } - - public bool TagHelpersLoading - { - get { return _tagHelpersLoading; } - set - { - _tagHelpersLoading = value; - OnPropertyChanged(); - OnPropertyChanged(nameof(TagHelperProgressVisibility)); - } - } - - public Visibility TagHelperProgressVisibility => TagHelpersLoading ? Visibility.Visible : Visibility.Hidden; - - } -} -#endif diff --git a/tooling/Microsoft.VisualStudio.RazorExtension/RazorInfo/ProjectPropertyCollectionViewModel.cs b/tooling/Microsoft.VisualStudio.RazorExtension/RazorInfo/ProjectPropertyCollectionViewModel.cs new file mode 100644 index 0000000000..4a3c63d088 --- /dev/null +++ b/tooling/Microsoft.VisualStudio.RazorExtension/RazorInfo/ProjectPropertyCollectionViewModel.cs @@ -0,0 +1,41 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +#if RAZOR_EXTENSION_DEVELOPER_MODE + +using System.Collections.ObjectModel; +using System.Linq; +using Microsoft.CodeAnalysis.Razor.ProjectSystem; + +namespace Microsoft.VisualStudio.RazorExtension.RazorInfo +{ + public class ProjectPropertyCollectionViewModel : NotifyPropertyChanged + { + private readonly ProjectSnapshot _project; + + internal ProjectPropertyCollectionViewModel(ProjectSnapshot project) + { + _project = project; + + Properties = new ObservableCollection(); + Properties.Add(new ProjectPropertyItemViewModel("Language Version", _project.Configuration?.LanguageVersion.ToString())); + Properties.Add(new ProjectPropertyItemViewModel("Configuration", FormatConfiguration(_project))); + Properties.Add(new ProjectPropertyItemViewModel("Extensions", FormatExtensions(_project))); + Properties.Add(new ProjectPropertyItemViewModel("Workspace Project", _project.WorkspaceProject?.Name)); + } + + public ObservableCollection Properties { get; } + + private static string FormatConfiguration(ProjectSnapshot project) + { + return $"{project.Configuration.ConfigurationName} ({project.Configuration.GetType().Name})"; + } + + private static string FormatExtensions(ProjectSnapshot project) + { + return $"{string.Join(", ", project.Configuration.Extensions.Select(e => e.ExtensionName))}"; + } + } +} + +#endif diff --git a/tooling/Microsoft.VisualStudio.RazorExtension/RazorInfo/PropertyViewModel.cs b/tooling/Microsoft.VisualStudio.RazorExtension/RazorInfo/ProjectPropertyItemViewModel.cs similarity index 74% rename from tooling/Microsoft.VisualStudio.RazorExtension/RazorInfo/PropertyViewModel.cs rename to tooling/Microsoft.VisualStudio.RazorExtension/RazorInfo/ProjectPropertyItemViewModel.cs index e7586847ac..4a229a461a 100644 --- a/tooling/Microsoft.VisualStudio.RazorExtension/RazorInfo/PropertyViewModel.cs +++ b/tooling/Microsoft.VisualStudio.RazorExtension/RazorInfo/ProjectPropertyItemViewModel.cs @@ -5,9 +5,9 @@ namespace Microsoft.VisualStudio.RazorExtension.RazorInfo { - public class PropertyViewModel : NotifyPropertyChanged + public class ProjectPropertyItemViewModel : NotifyPropertyChanged { - internal PropertyViewModel(string name, string value) + internal ProjectPropertyItemViewModel(string name, string value) { Name = name; Value = value; diff --git a/tooling/Microsoft.VisualStudio.RazorExtension/RazorInfo/ProjectSnapshotViewModel.cs b/tooling/Microsoft.VisualStudio.RazorExtension/RazorInfo/ProjectSnapshotViewModel.cs deleted file mode 100644 index 4d786de67a..0000000000 --- a/tooling/Microsoft.VisualStudio.RazorExtension/RazorInfo/ProjectSnapshotViewModel.cs +++ /dev/null @@ -1,55 +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.ObjectModel; -using System.IO; -using System.Linq; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Razor.ProjectSystem; - -namespace Microsoft.VisualStudio.RazorExtension.RazorInfo -{ - public class ProjectSnapshotViewModel : NotifyPropertyChanged - { - internal ProjectSnapshotViewModel(ProjectSnapshot project) - { - Project = project; - - Id = project.WorkspaceProject?.Id; - Properties = new ObservableCollection(); - - InitializeProperties(); - } - - internal ProjectSnapshot Project { get; } - - public string Name => Path.GetFileNameWithoutExtension(Project.FilePath); - - public ProjectId Id { get; } - - public ObservableCollection Properties { get; } - - private void InitializeProperties() - { - Properties.Clear(); - - Properties.Add(new PropertyViewModel("Language Version", Project.Configuration?.LanguageVersion.ToString())); - Properties.Add(new PropertyViewModel("Configuration", FormatConfiguration(Project))); - Properties.Add(new PropertyViewModel("Extensions", FormatExtensions(Project))); - Properties.Add(new PropertyViewModel("Workspace Project", Project.WorkspaceProject?.Name)); - } - - private static string FormatConfiguration(ProjectSnapshot project) - { - return $"{project.Configuration.ConfigurationName} ({project.Configuration.GetType().Name})"; - } - - private static string FormatExtensions(ProjectSnapshot project) - { - return $"{string.Join(", ", project.Configuration.Extensions.Select(e => e.ExtensionName))}"; - } - } -} -#endif \ No newline at end of file diff --git a/tooling/Microsoft.VisualStudio.RazorExtension/RazorInfo/ProjectViewModel.cs b/tooling/Microsoft.VisualStudio.RazorExtension/RazorInfo/ProjectViewModel.cs index 1d1442c05f..fe385d58bd 100644 --- a/tooling/Microsoft.VisualStudio.RazorExtension/RazorInfo/ProjectViewModel.cs +++ b/tooling/Microsoft.VisualStudio.RazorExtension/RazorInfo/ProjectViewModel.cs @@ -8,8 +8,6 @@ namespace Microsoft.VisualStudio.RazorExtension.RazorInfo { public class ProjectViewModel : NotifyPropertyChanged { - private ProjectSnapshotViewModel _snapshot; - internal ProjectViewModel(string filePath) { FilePath = filePath; @@ -18,19 +16,6 @@ namespace Microsoft.VisualStudio.RazorExtension.RazorInfo public string FilePath { get; } public string Name => Path.GetFileNameWithoutExtension(FilePath); - - public bool HasSnapshot => Snapshot != null; - - public ProjectSnapshotViewModel Snapshot - { - get => _snapshot; - set - { - _snapshot = value; - OnPropertyChanged(); - OnPropertyChanged(nameof(HasSnapshot)); - } - } } } #endif \ No newline at end of file diff --git a/tooling/Microsoft.VisualStudio.RazorExtension/RazorInfo/RazorInfoToolWindow.cs b/tooling/Microsoft.VisualStudio.RazorExtension/RazorInfo/RazorInfoToolWindow.cs index c391b59a6c..f7ac4d9e2a 100644 --- a/tooling/Microsoft.VisualStudio.RazorExtension/RazorInfo/RazorInfoToolWindow.cs +++ b/tooling/Microsoft.VisualStudio.RazorExtension/RazorInfo/RazorInfoToolWindow.cs @@ -42,18 +42,16 @@ namespace Microsoft.VisualStudio.RazorExtension.RazorInfo _projectManager = _workspace.Services.GetLanguageServices(RazorLanguage.Name).GetRequiredService(); _projectManager.Changed += ProjectManager_Changed; - DataContext = new RazorInfoViewModel(this, _workspace, _projectManager, OnException); + DataContext = new RazorInfoViewModel(_workspace, _projectManager, OnException); + foreach (var project in _projectManager.Projects) { - DataContext.Projects.Add(new ProjectViewModel(project.FilePath) - { - Snapshot = new ProjectSnapshotViewModel(project), - }); + DataContext.Projects.Add(new ProjectViewModel(project.FilePath)); } if (DataContext.Projects.Count > 0) { - DataContext.CurrentProject = DataContext.Projects[0]; + DataContext.SelectedProject = DataContext.Projects[0]; } } @@ -69,70 +67,7 @@ namespace Microsoft.VisualStudio.RazorExtension.RazorInfo private void ProjectManager_Changed(object sender, ProjectChangeEventArgs e) { - switch (e.Kind) - { - case ProjectChangeKind.ProjectAdded: - { - var added = new ProjectViewModel(e.ProjectFilePath) - { - Snapshot = new ProjectSnapshotViewModel(_projectManager.GetLoadedProject(e.ProjectFilePath)), - }; - - DataContext.Projects.Add(added); - - if (DataContext.Projects.Count == 1) - { - DataContext.CurrentProject = added; - } - break; - } - - case ProjectChangeKind.ProjectRemoved: - { - ProjectViewModel removed = null; - for (var i = DataContext.Projects.Count - 1; i >= 0; i--) - { - var project = DataContext.Projects[i]; - if (project.FilePath == e.ProjectFilePath) - { - removed = project; - DataContext.Projects.RemoveAt(i); - break; - } - } - - if (DataContext.CurrentProject == removed) - { - DataContext.CurrentProject = null; - } - - break; - } - - case ProjectChangeKind.ProjectChanged: - case ProjectChangeKind.DocumentsChanged: - { - ProjectViewModel changed = null; - for (var i = DataContext.Projects.Count - 1; i >= 0; i--) - { - var project = DataContext.Projects[i]; - if (project.FilePath == e.ProjectFilePath) - { - changed = project; - changed.Snapshot = new ProjectSnapshotViewModel(_projectManager.GetLoadedProject(e.ProjectFilePath)); - DataContext.LoadProjectInfo(); - break; - } - } - - break; - } - - case ProjectChangeKind.DocumentContentChanged: - { - break; - } - } + DataContext.OnChange(e); } private void OnException(Exception ex) diff --git a/tooling/Microsoft.VisualStudio.RazorExtension/RazorInfo/RazorInfoToolWindowControl.xaml b/tooling/Microsoft.VisualStudio.RazorExtension/RazorInfo/RazorInfoToolWindowControl.xaml index 5ebd94fae5..f7fd186d6e 100644 --- a/tooling/Microsoft.VisualStudio.RazorExtension/RazorInfo/RazorInfoToolWindowControl.xaml +++ b/tooling/Microsoft.VisualStudio.RazorExtension/RazorInfo/RazorInfoToolWindowControl.xaml @@ -13,6 +13,7 @@ d:DesignWidth="300" Name="RazorInfoToolWindow"> +