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"> +