diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultProjectSnapshot.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultProjectSnapshot.cs new file mode 100644 index 0000000000..c8429a777d --- /dev/null +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultProjectSnapshot.cs @@ -0,0 +1,22 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +namespace Microsoft.CodeAnalysis.Razor.ProjectSystem +{ + internal class DefaultProjectSnapshot : ProjectSnapshot + { + public DefaultProjectSnapshot(Project underlyingProject) + { + if (underlyingProject == null) + { + throw new ArgumentNullException(nameof(underlyingProject)); + } + + UnderlyingProject = underlyingProject; + } + + public override Project UnderlyingProject { get; } + } +} diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultProjectSnapshotListener.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultProjectSnapshotListener.cs new file mode 100644 index 0000000000..79ef8206d2 --- /dev/null +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultProjectSnapshotListener.cs @@ -0,0 +1,21 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +namespace Microsoft.CodeAnalysis.Razor.ProjectSystem +{ + internal class DefaultProjectSnapshotListener : ProjectSnapshotListener + { + public override event EventHandler ProjectChanged; + + internal void Notify(ProjectChangeEventArgs e) + { + var handler = ProjectChanged; + if (handler != null) + { + handler(this, e); + } + } + } +} diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultProjectSnapshotManager.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultProjectSnapshotManager.cs new file mode 100644 index 0000000000..c113bf0264 --- /dev/null +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultProjectSnapshotManager.cs @@ -0,0 +1,146 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; + +namespace Microsoft.CodeAnalysis.Razor.ProjectSystem +{ + internal class DefaultProjectSnapshotManager : ProjectSnapshotManager + { + private readonly Workspace _workspace; + private readonly Dictionary _projects; + private readonly List> _listeners; + + public DefaultProjectSnapshotManager(Workspace workspace) + { + _workspace = workspace; + + _projects = new Dictionary(); + _listeners = new List>(); + + // Attaching the event handler inside before initialization prevents re-entrancy without + // losing any notifications. + _workspace.WorkspaceChanged += Workspace_WorkspaceChanged; + InitializeSolution(_workspace.CurrentSolution); + } + + public override IReadOnlyList Projects => _projects.Values.ToArray(); + + public override ProjectSnapshot FindProject(string projectPath) + { + if (projectPath == null) + { + throw new ArgumentNullException(nameof(projectPath)); + } + + foreach (var project in _projects.Values) + { + if (string.Equals(projectPath, project.UnderlyingProject.FilePath, StringComparison.OrdinalIgnoreCase)) + { + return project; + } + } + + return null; + } + + public override ProjectSnapshotListener Subscribe() + { + var subscription = new DefaultProjectSnapshotListener(); + _listeners.Add(new WeakReference(subscription)); + + return subscription; + } + + private void InitializeSolution(Solution solution) + { + Debug.Assert(solution != null); + + foreach (var kvp in _projects.ToArray()) + { + _projects.Remove(kvp.Key); + NotifyListeners(new ProjectChangeEventArgs(kvp.Value, ProjectChangeKind.Removed)); + } + + foreach (var project in solution.Projects) + { + var projectState = new DefaultProjectSnapshot(project); + _projects[project.Id] = projectState; + + NotifyListeners(new ProjectChangeEventArgs(projectState, ProjectChangeKind.Added)); + } + } + + private void NotifyListeners(ProjectChangeEventArgs e) + { + for (var i = 0; i < _listeners.Count; i++) + { + if (_listeners[i].TryGetTarget(out var listener)) + { + listener.Notify(e); + } + else + { + _listeners.RemoveAt(i--); + } + } + } + + // Internal for testing + internal void Workspace_WorkspaceChanged(object sender, WorkspaceChangeEventArgs e) + { + Project underlyingProject; + ProjectSnapshot snapshot; + switch (e.Kind) + { + case WorkspaceChangeKind.ProjectAdded: + { + underlyingProject = e.NewSolution.GetProject(e.ProjectId); + Debug.Assert(underlyingProject != null); + + snapshot = new DefaultProjectSnapshot(underlyingProject); + _projects[e.ProjectId] = snapshot; + + NotifyListeners(new ProjectChangeEventArgs(snapshot, ProjectChangeKind.Added)); + break; + } + + case WorkspaceChangeKind.ProjectChanged: + case WorkspaceChangeKind.ProjectReloaded: + { + underlyingProject = e.NewSolution.GetProject(e.ProjectId); + Debug.Assert(underlyingProject != null); + + snapshot = new DefaultProjectSnapshot(underlyingProject); + _projects[e.ProjectId] = snapshot; + + NotifyListeners(new ProjectChangeEventArgs(snapshot, ProjectChangeKind.Changed)); + break; + } + + case WorkspaceChangeKind.ProjectRemoved: + { + // We're being extra defensive here to avoid crashes. + if (_projects.TryGetValue(e.ProjectId, out snapshot)) + { + _projects.Remove(e.ProjectId); + NotifyListeners(new ProjectChangeEventArgs(snapshot, ProjectChangeKind.Removed)); + } + + break; + } + + case WorkspaceChangeKind.SolutionAdded: + case WorkspaceChangeKind.SolutionChanged: + case WorkspaceChangeKind.SolutionCleared: + case WorkspaceChangeKind.SolutionReloaded: + case WorkspaceChangeKind.SolutionRemoved: + InitializeSolution(e.NewSolution); + break; + } + } + } +} diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultProjectSnapshotManagerFactory.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultProjectSnapshotManagerFactory.cs new file mode 100644 index 0000000000..858dea3347 --- /dev/null +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultProjectSnapshotManagerFactory.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 System.Composition; +using Microsoft.CodeAnalysis.Host; +using Microsoft.CodeAnalysis.Host.Mef; + +namespace Microsoft.CodeAnalysis.Razor.ProjectSystem +{ + [Shared] + [ExportLanguageServiceFactory(typeof(ProjectSnapshotManager), RazorLanguage.Name)] + internal class DefaultProjectSnapshotManagerFactory : ILanguageServiceFactory + { + public ILanguageService CreateLanguageService(HostLanguageServices languageServices) + { + if (languageServices == null) + { + throw new ArgumentNullException(nameof(languageServices)); + } + + return new DefaultProjectSnapshotManager(languageServices.WorkspaceServices.Workspace); + } + } +} diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectChangeEventArgs.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectChangeEventArgs.cs new file mode 100644 index 0000000000..eca6b56774 --- /dev/null +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectChangeEventArgs.cs @@ -0,0 +1,20 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +namespace Microsoft.CodeAnalysis.Razor.ProjectSystem +{ + internal class ProjectChangeEventArgs : EventArgs + { + public ProjectChangeEventArgs(ProjectSnapshot project, ProjectChangeKind kind) + { + Project = project; + Kind = kind; + } + + public ProjectSnapshot Project { 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 new file mode 100644 index 0000000000..000ebd1953 --- /dev/null +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectChangeKind.cs @@ -0,0 +1,12 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.CodeAnalysis.Razor.ProjectSystem +{ + internal enum ProjectChangeKind + { + Added, + Removed, + Changed, + } +} diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSnapshot.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSnapshot.cs new file mode 100644 index 0000000000..84792c2b47 --- /dev/null +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSnapshot.cs @@ -0,0 +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. + +namespace Microsoft.CodeAnalysis.Razor.ProjectSystem +{ + internal abstract class ProjectSnapshot + { + public abstract Project UnderlyingProject { get; } + } +} diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSnapshotListener.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSnapshotListener.cs new file mode 100644 index 0000000000..08e3c295d9 --- /dev/null +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSnapshotListener.cs @@ -0,0 +1,12 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +namespace Microsoft.CodeAnalysis.Razor.ProjectSystem +{ + internal abstract class ProjectSnapshotListener + { + public abstract event EventHandler ProjectChanged; + } +} diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSnapshotManager.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSnapshotManager.cs new file mode 100644 index 0000000000..9ebb76961a --- /dev/null +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSnapshotManager.cs @@ -0,0 +1,17 @@ +// 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 Microsoft.CodeAnalysis.Host; + +namespace Microsoft.CodeAnalysis.Razor.ProjectSystem +{ + internal abstract class ProjectSnapshotManager : ILanguageService + { + public abstract IReadOnlyList Projects { get; } + + public abstract ProjectSnapshot FindProject(string projectPath); + + public abstract ProjectSnapshotListener Subscribe(); + } +} diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/Properties/AssemblyInfo.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/Properties/AssemblyInfo.cs index 25fe6135fa..f3ea1dc4ee 100644 --- a/src/Microsoft.CodeAnalysis.Razor.Workspaces/Properties/AssemblyInfo.cs +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/Properties/AssemblyInfo.cs @@ -6,5 +6,6 @@ using System.Runtime.CompilerServices; [assembly: InternalsVisibleTo("Microsoft.CodeAnalysis.Razor.Workspaces.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] [assembly: InternalsVisibleTo("Microsoft.CodeAnalysis.Remote.Razor, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] [assembly: InternalsVisibleTo("Microsoft.VisualStudio.LanguageServices.Razor, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] +[assembly: InternalsVisibleTo("Microsoft.VisualStudio.LanguageServices.Razor.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] [assembly: InternalsVisibleTo("Microsoft.VisualStudio.RazorExtension, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")] diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/Editor/DefaultTextBufferProjectService.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/Editor/DefaultTextBufferProjectService.cs index 462ef99d97..fa2b068990 100644 --- a/src/Microsoft.VisualStudio.LanguageServices.Razor/Editor/DefaultTextBufferProjectService.cs +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/Editor/DefaultTextBufferProjectService.cs @@ -61,6 +61,17 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor return hierarchy; } + public override string GetProjectPath(IVsHierarchy hierarchy) + { + if (hierarchy == null) + { + throw new ArgumentNullException(nameof(hierarchy)); + } + + ErrorHandler.ThrowOnFailure(((IVsProject)hierarchy).GetMkDocument((uint)VSConstants.VSITEMID.Root, out var path), VSConstants.E_NOTIMPL); + return path; + } + public override bool IsSupportedProject(IVsHierarchy hierarchy) { if (hierarchy == null) diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/Editor/DefaultVisualStudioDocumentTracker.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/Editor/DefaultVisualStudioDocumentTracker.cs index 2e673956e5..f47e2ad6f9 100644 --- a/src/Microsoft.VisualStudio.LanguageServices.Razor/Editor/DefaultVisualStudioDocumentTracker.cs +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/Editor/DefaultVisualStudioDocumentTracker.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Razor; +using Microsoft.CodeAnalysis.Razor.ProjectSystem; using Microsoft.VisualStudio.Shell.Interop; using Microsoft.VisualStudio.Text; using Microsoft.VisualStudio.Text.Editor; @@ -13,17 +14,30 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor { internal class DefaultVisualStudioDocumentTracker : VisualStudioDocumentTracker { + private readonly ProjectSnapshotManager _projectManager; private readonly TextBufferProjectService _projectService; private readonly ITextBuffer _textBuffer; private readonly List _textViews; - + private readonly Workspace _workspace; + private bool _isSupportedProject; - private Workspace _workspace; + private ProjectSnapshot _project; + private string _projectPath; + private ProjectSnapshotListener _subscription; public override event EventHandler ContextChanged; - public DefaultVisualStudioDocumentTracker(TextBufferProjectService projectService, Workspace workspace, ITextBuffer textBuffer) + public DefaultVisualStudioDocumentTracker( + ProjectSnapshotManager projectManager, + TextBufferProjectService projectService, + Workspace workspace, + ITextBuffer textBuffer) { + if (projectManager == null) + { + throw new ArgumentNullException(nameof(projectManager)); + } + if (projectService == null) { throw new ArgumentNullException(nameof(projectService)); @@ -39,18 +53,19 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor throw new ArgumentNullException(nameof(textBuffer)); } + _projectManager = projectManager; _projectService = projectService; _textBuffer = textBuffer; - _workspace = workspace; + _workspace = workspace; // For now we assume that the workspace is the always default VS workspace. _textViews = new List(); - - Update(); + + Initialize(); } public override bool IsSupportedProject => _isSupportedProject; - public override ProjectId ProjectId => null; + public override Project Project => _project?.UnderlyingProject; public override ITextBuffer TextBuffer => _textBuffer; @@ -60,49 +75,61 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor public override Workspace Workspace => _workspace; - private bool Update() + private void Initialize() { - // Update is called when the state of any of our surrounding systems changes. Here we want to examine the - // state of the world and then update properties as necessary. - // // Fundamentally we have a Razor half of the world as as soon as the document is open - and then later // the C# half of the world will be initialized. This code is in general pretty tolerant of // unexpected /impossible states. // - // We also want to successfully shut down when the buffer is renamed to something other .cshtml. - IVsHierarchy project = null; + // We also want to successfully shut down if the buffer is something other than .cshtml. + IVsHierarchy hierarchy = null; + string projectPath = null; var isSupportedProject = false; + if (_textBuffer.ContentType.IsOfType(RazorLanguage.ContentType) && - (project = _projectService.GetHierarchy(_textBuffer)) != null) - { + // We expect the document to have a hierarchy even if it's not a real 'project'. // However the hierarchy can be null when the document is in the process of closing. - isSupportedProject = _projectService.IsSupportedProject(project); - } - - // For now we temporarily assume that the workspace is the default VS workspace. - var workspace = _workspace; - - var changed = false; - changed |= isSupportedProject == _isSupportedProject; - changed |= workspace == _workspace; - - if (changed) + (hierarchy = _projectService.GetHierarchy(_textBuffer)) != null) { - _isSupportedProject = isSupportedProject; - _workspace = workspace; + projectPath = _projectService.GetProjectPath(hierarchy); + isSupportedProject = _projectService.IsSupportedProject(hierarchy); } - return changed; - } + if (!isSupportedProject || projectPath == null) + { + return; + } - private void OnContextChanged() + var project = _projectManager.FindProject(projectPath); + + var subscription = _projectManager.Subscribe(); + subscription.ProjectChanged += Subscription_ProjectStateChanged; + + _isSupportedProject = isSupportedProject; + _projectPath = projectPath; + _project = project; + _subscription = subscription; + } + + private void OnContextChanged(ProjectSnapshot project) { + _project = project; + var handler = ContextChanged; if (handler != null) { handler(this, EventArgs.Empty); } } + + private void Subscription_ProjectStateChanged(object sender, ProjectChangeEventArgs e) + { + if (_projectPath != null && + string.Equals(_projectPath, e.Project.UnderlyingProject.FilePath, StringComparison.OrdinalIgnoreCase)) + { + OnContextChanged(e.Project); + } + } } } diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/Editor/DefaultVisualStudioDocumentTrackerFactory.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/Editor/DefaultVisualStudioDocumentTrackerFactory.cs index 68bce7a1c2..de000a998c 100644 --- a/src/Microsoft.VisualStudio.LanguageServices.Razor/Editor/DefaultVisualStudioDocumentTrackerFactory.cs +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/Editor/DefaultVisualStudioDocumentTrackerFactory.cs @@ -8,9 +8,9 @@ using System.Diagnostics; using System.Linq; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Razor; +using Microsoft.CodeAnalysis.Razor.ProjectSystem; using Microsoft.VisualStudio.Text; using Microsoft.VisualStudio.Text.Editor; -using Microsoft.VisualStudio.Text.Projection; using Microsoft.VisualStudio.Utilities; namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor @@ -21,6 +21,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor [Export(typeof(VisualStudioDocumentTrackerFactory))] internal class DefaultVisualStudioDocumentTrackerFactory : VisualStudioDocumentTrackerFactory, IWpfTextViewConnectionListener { + private readonly ProjectSnapshotManager _projectManager; private readonly TextBufferProjectService _projectService; private readonly Workspace _workspace; @@ -41,6 +42,34 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor _projectService = projectService; _workspace = workspace; + + _projectManager = workspace.Services.GetLanguageServices(RazorLanguage.Name).GetRequiredService(); + } + + // This is only for testing. We want to avoid using the actual Roslyn GetService methods in unit tests. + internal DefaultVisualStudioDocumentTrackerFactory( + ProjectSnapshotManager projectManager, + TextBufferProjectService projectService, + [Import(typeof(VisualStudioWorkspace))] Workspace workspace) + { + if (projectManager == null) + { + throw new ArgumentNullException(nameof(projectManager)); + } + + if (projectService == null) + { + throw new ArgumentNullException(nameof(projectService)); + } + + if (workspace == null) + { + throw new ArgumentNullException(nameof(workspace)); + } + + _projectManager = projectManager; + _projectService = projectService; + _workspace = workspace; } public Workspace Workspace => _workspace; @@ -96,7 +125,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor DefaultVisualStudioDocumentTracker tracker; if (!textBuffer.Properties.TryGetProperty(typeof(VisualStudioDocumentTracker), out tracker)) { - tracker = new DefaultVisualStudioDocumentTracker(_projectService, _workspace, textBuffer); + tracker = new DefaultVisualStudioDocumentTracker(_projectManager, _projectService, _workspace, textBuffer); textBuffer.Properties.AddProperty(typeof(VisualStudioDocumentTracker), tracker); } diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/Editor/TextBufferProjectService.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/Editor/TextBufferProjectService.cs index e85298a376..89a5eb49cd 100644 --- a/src/Microsoft.VisualStudio.LanguageServices.Razor/Editor/TextBufferProjectService.cs +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/Editor/TextBufferProjectService.cs @@ -11,5 +11,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor public abstract IVsHierarchy GetHierarchy(ITextBuffer textBuffer); public abstract bool IsSupportedProject(IVsHierarchy hierarchy); + + public abstract string GetProjectPath(IVsHierarchy hierarchy); } } diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/Editor/VisualStudioDocumentTracker.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/Editor/VisualStudioDocumentTracker.cs index 97ab37285a..50551997fd 100644 --- a/src/Microsoft.VisualStudio.LanguageServices.Razor/Editor/VisualStudioDocumentTracker.cs +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/Editor/VisualStudioDocumentTracker.cs @@ -15,7 +15,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor public abstract bool IsSupportedProject { get; } - public abstract ProjectId ProjectId { get; } + public abstract Project Project { get; } public abstract Workspace Workspace { get; } diff --git a/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Microsoft.CodeAnalysis.Razor.Workspaces.Test.csproj b/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Microsoft.CodeAnalysis.Razor.Workspaces.Test.csproj index 1dde803a83..2ec05253b1 100644 --- a/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Microsoft.CodeAnalysis.Razor.Workspaces.Test.csproj +++ b/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Microsoft.CodeAnalysis.Razor.Workspaces.Test.csproj @@ -18,8 +18,10 @@ + + diff --git a/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/ProjectSystem/DefaultProjectStateManagerTest.cs b/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/ProjectSystem/DefaultProjectStateManagerTest.cs new file mode 100644 index 0000000000..e20c21a4f7 --- /dev/null +++ b/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/ProjectSystem/DefaultProjectStateManagerTest.cs @@ -0,0 +1,157 @@ +// 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.Linq; +using Xunit; + +namespace Microsoft.CodeAnalysis.Razor.ProjectSystem +{ + public class DefaultProjectStateManagerTest + { + public DefaultProjectStateManagerTest() + { + Workspace = new AdhocWorkspace(); + EmptySolution = Workspace.CurrentSolution.GetIsolatedSolution(); + + ProjectNumberOne = Workspace.CurrentSolution.AddProject("One", "One", LanguageNames.CSharp); + ProjectNumberTwo = ProjectNumberOne.Solution.AddProject("Two", "Two", LanguageNames.CSharp); + SolutionWithTwoProjects = ProjectNumberTwo.Solution; + + ProjectNumberThree = EmptySolution.GetIsolatedSolution().AddProject("Three", "Three", LanguageNames.CSharp); + SolutionWithOneProject = ProjectNumberThree.Solution; + } + + private Solution EmptySolution { get; } + + private Solution SolutionWithOneProject { get; } + + private Solution SolutionWithTwoProjects { get; } + + private Project ProjectNumberOne { get; } + + private Project ProjectNumberTwo { get; } + + private Project ProjectNumberThree { get; } + + private Workspace Workspace { get; } + + [Theory] + [InlineData(WorkspaceChangeKind.SolutionAdded)] + [InlineData(WorkspaceChangeKind.SolutionChanged)] + [InlineData(WorkspaceChangeKind.SolutionCleared)] + [InlineData(WorkspaceChangeKind.SolutionReloaded)] + [InlineData(WorkspaceChangeKind.SolutionRemoved)] + public void WorkspaceChanged_SolutionEvents_AddsProjectsInSolution(WorkspaceChangeKind kind) + { + // Arrange + var projectManager = new DefaultProjectSnapshotManager(Workspace); + + var e = new WorkspaceChangeEventArgs(kind, oldSolution: EmptySolution, newSolution: SolutionWithTwoProjects); + + // Act + projectManager.Workspace_WorkspaceChanged(Workspace, e); + + // Assert + Assert.Collection( + projectManager.Projects.OrderBy(p => p.UnderlyingProject.Name), + p => Assert.Equal(ProjectNumberOne.Id, p.UnderlyingProject.Id), + p => Assert.Equal(ProjectNumberTwo.Id, p.UnderlyingProject.Id)); + } + + [Theory] + [InlineData(WorkspaceChangeKind.SolutionAdded)] + [InlineData(WorkspaceChangeKind.SolutionChanged)] + [InlineData(WorkspaceChangeKind.SolutionCleared)] + [InlineData(WorkspaceChangeKind.SolutionReloaded)] + [InlineData(WorkspaceChangeKind.SolutionRemoved)] + public void WorkspaceChanged_SolutionEvents_ClearsExistingProjects_AddsProjectsInSolution(WorkspaceChangeKind kind) + { + // Arrange + var projectManager = new DefaultProjectSnapshotManager(Workspace); + + // Initialize with a project. This will get removed. + var e = new WorkspaceChangeEventArgs(WorkspaceChangeKind.SolutionAdded, oldSolution: EmptySolution, newSolution: SolutionWithOneProject); + projectManager.Workspace_WorkspaceChanged(Workspace, e); + + e = new WorkspaceChangeEventArgs(kind, oldSolution: EmptySolution, newSolution: SolutionWithTwoProjects); + + // Act + projectManager.Workspace_WorkspaceChanged(Workspace, e); + + // Assert + Assert.Collection( + projectManager.Projects.OrderBy(p => p.UnderlyingProject.Name), + p => Assert.Equal(ProjectNumberOne.Id, p.UnderlyingProject.Id), + p => Assert.Equal(ProjectNumberTwo.Id, p.UnderlyingProject.Id)); + } + + [Theory] + [InlineData(WorkspaceChangeKind.ProjectChanged)] + [InlineData(WorkspaceChangeKind.ProjectReloaded)] + public void WorkspaceChanged_ProjectChangeEvents_UpdatesProject(WorkspaceChangeKind kind) + { + // Arrange + var projectManager = new DefaultProjectSnapshotManager(Workspace); + + // Initialize with some projects. + var e = new WorkspaceChangeEventArgs(WorkspaceChangeKind.SolutionAdded, oldSolution: EmptySolution, newSolution: SolutionWithTwoProjects); + projectManager.Workspace_WorkspaceChanged(Workspace, e); + + var solution = SolutionWithTwoProjects.WithProjectAssemblyName(ProjectNumberOne.Id, "Changed"); + e = new WorkspaceChangeEventArgs(kind, oldSolution: SolutionWithTwoProjects, newSolution: solution, projectId: ProjectNumberOne.Id); + + // Act + projectManager.Workspace_WorkspaceChanged(Workspace, e); + + // Assert + Assert.Collection( + projectManager.Projects.OrderBy(p => p.UnderlyingProject.Name), + p => + { + Assert.Equal(ProjectNumberOne.Id, p.UnderlyingProject.Id); + Assert.Equal("Changed", p.UnderlyingProject.AssemblyName); + }, + p => Assert.Equal(ProjectNumberTwo.Id, p.UnderlyingProject.Id)); + } + + [Fact] + public void WorkspaceChanged_ProjectRemovedEvent_RemovesProject() + { + // Arrange + var projectManager = new DefaultProjectSnapshotManager(Workspace); + + // Initialize with some projects project. + var e = new WorkspaceChangeEventArgs(WorkspaceChangeKind.SolutionAdded, oldSolution: EmptySolution, newSolution: SolutionWithTwoProjects); + projectManager.Workspace_WorkspaceChanged(Workspace, e); + + var solution = SolutionWithTwoProjects.RemoveProject(ProjectNumberOne.Id); + e = new WorkspaceChangeEventArgs(WorkspaceChangeKind.ProjectRemoved, oldSolution: SolutionWithTwoProjects, newSolution: solution, projectId: ProjectNumberOne.Id); + + // Act + projectManager.Workspace_WorkspaceChanged(Workspace, e); + + // Assert + Assert.Collection( + projectManager.Projects.OrderBy(p => p.UnderlyingProject.Name), + p => Assert.Equal(ProjectNumberTwo.Id, p.UnderlyingProject.Id)); + } + + [Fact] + public void WorkspaceChanged_ProjectAddedEvent_AddsProject() + { + // Arrange + var projectManager = new DefaultProjectSnapshotManager(Workspace); + + var solution = SolutionWithOneProject; + var e = new WorkspaceChangeEventArgs(WorkspaceChangeKind.ProjectAdded, oldSolution: EmptySolution, newSolution: solution, projectId: ProjectNumberThree.Id); + + // Act + projectManager.Workspace_WorkspaceChanged(Workspace, e); + + // Assert + Assert.Collection( + projectManager.Projects.OrderBy(p => p.UnderlyingProject.Name), + p => Assert.Equal(ProjectNumberThree.Id, p.UnderlyingProject.Id)); + } + } +} diff --git a/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Editor/DefaultVisualStudioDocumentTrackerFactoryTest.cs b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Editor/DefaultVisualStudioDocumentTrackerFactoryTest.cs index c7d4ada5af..1025ed98f7 100644 --- a/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Editor/DefaultVisualStudioDocumentTrackerFactoryTest.cs +++ b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Editor/DefaultVisualStudioDocumentTrackerFactoryTest.cs @@ -5,6 +5,7 @@ using System; using System.Collections.ObjectModel; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Razor; +using Microsoft.CodeAnalysis.Razor.ProjectSystem; using Microsoft.VisualStudio.Shell.Interop; using Microsoft.VisualStudio.Text; using Microsoft.VisualStudio.Text.Editor; @@ -17,9 +18,13 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor { public class DefaultVisualStudioDocumentTrackerFactoryTest { + private ProjectSnapshotManager ProjectManager { get; } = Mock.Of( + p => p.FindProject(It.IsAny()) == Mock.Of() && + p.Subscribe() == Mock.Of()); + private TextBufferProjectService ProjectService { get; } = Mock.Of( - s => s.GetHierarchy(It.IsAny()) == Mock.Of() && - s.IsSupportedProject(It.IsAny()) == true); + s => s.GetHierarchy(It.IsAny()) == Mock.Of() && + s.IsSupportedProject(It.IsAny()) == true); private Workspace Workspace { get; } = new AdhocWorkspace(); @@ -31,7 +36,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor public void SubjectBuffersConnected_ForNonRazorTextBuffer_DoesNothing() { // Arrange - var factory = new DefaultVisualStudioDocumentTrackerFactory(ProjectService, Workspace); + var factory = new DefaultVisualStudioDocumentTrackerFactory(ProjectManager, ProjectService, Workspace); var textView = Mock.Of(); @@ -51,7 +56,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor public void SubjectBuffersConnected_ForRazorTextBufferWithoutTracker_CreatesTrackerAndTracksTextView() { // Arrange - var factory = new DefaultVisualStudioDocumentTrackerFactory(ProjectService, Workspace); + var factory = new DefaultVisualStudioDocumentTrackerFactory(ProjectManager, ProjectService, Workspace); var textView = Mock.Of(); @@ -73,7 +78,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor public void SubjectBuffersConnected_ForRazorTextBufferWithoutTracker_CreatesTrackerAndTracksTextView_ForMultipleBuffers() { // Arrange - var factory = new DefaultVisualStudioDocumentTrackerFactory(ProjectService, Workspace); + var factory = new DefaultVisualStudioDocumentTrackerFactory(ProjectManager, ProjectService, Workspace); var textView = Mock.Of(); @@ -103,7 +108,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor public void SubjectBuffersConnected_ForRazorTextBufferWithTracker_DoesNotAddDuplicateTextViewEntry() { // Arrange - var factory = new DefaultVisualStudioDocumentTrackerFactory(ProjectService, Workspace); + var factory = new DefaultVisualStudioDocumentTrackerFactory(ProjectManager, ProjectService, Workspace); var textView = Mock.Of(); @@ -113,7 +118,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor }; // Preload the buffer's properties with a tracker, so it's like we've already tracked this one. - var tracker = new DefaultVisualStudioDocumentTracker(ProjectService, Workspace, buffers[0]); + var tracker = new DefaultVisualStudioDocumentTracker(ProjectManager, ProjectService, Workspace, buffers[0]); tracker.TextViewsInternal.Add(textView); buffers[0].Properties.AddProperty(typeof(VisualStudioDocumentTracker), tracker); @@ -129,7 +134,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor public void SubjectBuffersConnected_ForRazorTextBufferWithTracker_AddsEntryForADifferentTextView() { // Arrange - var factory = new DefaultVisualStudioDocumentTrackerFactory(ProjectService, Workspace); + var factory = new DefaultVisualStudioDocumentTrackerFactory(ProjectManager, ProjectService, Workspace); var textView1 = Mock.Of(); var textView2 = Mock.Of(); @@ -140,7 +145,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor }; // Preload the buffer's properties with a tracker, so it's like we've already tracked this one. - var tracker = new DefaultVisualStudioDocumentTracker(ProjectService, Workspace, buffers[0]); + var tracker = new DefaultVisualStudioDocumentTracker(ProjectManager, ProjectService, Workspace, buffers[0]); tracker.TextViewsInternal.Add(textView1); buffers[0].Properties.AddProperty(typeof(VisualStudioDocumentTracker), tracker); @@ -156,7 +161,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor public void SubjectBuffersDisconnected_ForAnyTextBufferWithTracker_RemovesTextView() { // Arrange - var factory = new DefaultVisualStudioDocumentTrackerFactory(ProjectService, Workspace); + var factory = new DefaultVisualStudioDocumentTrackerFactory(ProjectManager, ProjectService, Workspace); var textView1 = Mock.Of(); var textView2 = Mock.Of(); @@ -168,12 +173,12 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor }; // Preload the buffer's properties with a tracker, so it's like we've already tracked this one. - var tracker = new DefaultVisualStudioDocumentTracker(ProjectService, Workspace, buffers[0]); + var tracker = new DefaultVisualStudioDocumentTracker(ProjectManager, ProjectService, Workspace, buffers[0]); tracker.TextViewsInternal.Add(textView1); tracker.TextViewsInternal.Add(textView2); buffers[0].Properties.AddProperty(typeof(VisualStudioDocumentTracker), tracker); - tracker = new DefaultVisualStudioDocumentTracker(ProjectService, Workspace, buffers[1]); + tracker = new DefaultVisualStudioDocumentTracker(ProjectManager, ProjectService, Workspace, buffers[1]); tracker.TextViewsInternal.Add(textView1); tracker.TextViewsInternal.Add(textView2); buffers[1].Properties.AddProperty(typeof(VisualStudioDocumentTracker), tracker); @@ -193,7 +198,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor public void SubjectBuffersDisconnected_ForAnyTextBufferWithoutTracker_DoesNothing() { // Arrange - var factory = new DefaultVisualStudioDocumentTrackerFactory(ProjectService, Workspace); + var factory = new DefaultVisualStudioDocumentTrackerFactory(ProjectManager, ProjectService, Workspace); var textView = Mock.Of(); @@ -213,7 +218,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor public void GetTracker_ForRazorTextBufferWithTracker_ReturnsTheFirstTracker() { // Arrange - var factory = new DefaultVisualStudioDocumentTrackerFactory(ProjectService, Workspace); + var factory = new DefaultVisualStudioDocumentTrackerFactory(ProjectManager, ProjectService, Workspace); var buffers = new Collection() { @@ -225,7 +230,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor var textView = Mock.Of(v => v.BufferGraph == bufferGraph); // Preload the buffer's properties with a tracker, so it's like we've already tracked this one. - var tracker = new DefaultVisualStudioDocumentTracker(ProjectService, Workspace, buffers[0]); + var tracker = new DefaultVisualStudioDocumentTracker(ProjectManager, ProjectService, Workspace, buffers[0]); tracker.TextViewsInternal.Add(textView); buffers[0].Properties.AddProperty(typeof(VisualStudioDocumentTracker), tracker); @@ -240,7 +245,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor public void GetTracker_WithoutRazorBuffer_ReturnsNull() { // Arrange - var factory = new DefaultVisualStudioDocumentTrackerFactory(ProjectService, Workspace); + var factory = new DefaultVisualStudioDocumentTrackerFactory(ProjectManager, ProjectService, Workspace); var buffers = new Collection(); diff --git a/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Microsoft.VisualStudio.LanguageServices.Razor.Test.csproj b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Microsoft.VisualStudio.LanguageServices.Razor.Test.csproj index 17966eee22..e32df25ebe 100644 --- a/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Microsoft.VisualStudio.LanguageServices.Razor.Test.csproj +++ b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Microsoft.VisualStudio.LanguageServices.Razor.Test.csproj @@ -23,6 +23,7 @@ + diff --git a/tooling/Microsoft.VisualStudio.RazorExtension/DocumentInfo/RazorDocumentInfoViewModel.cs b/tooling/Microsoft.VisualStudio.RazorExtension/DocumentInfo/RazorDocumentInfoViewModel.cs index 7027cbeca8..a3c8e85efb 100644 --- a/tooling/Microsoft.VisualStudio.RazorExtension/DocumentInfo/RazorDocumentInfoViewModel.cs +++ b/tooling/Microsoft.VisualStudio.RazorExtension/DocumentInfo/RazorDocumentInfoViewModel.cs @@ -40,7 +40,7 @@ namespace Microsoft.VisualStudio.RazorExtension.DocumentInfo } } - public ProjectId ProjectId => _documentTracker.ProjectId; + public ProjectId ProjectId => _documentTracker.Project?.Id; public Workspace Workspace => _documentTracker.Workspace; @@ -55,7 +55,6 @@ namespace Microsoft.VisualStudio.RazorExtension.DocumentInfo public TagHelperCompletionService TagHelperCompletionService => RazorLanguageServices?.GetRequiredService(); public TagHelperFactsService TagHelperFactsService => RazorLanguageServices?.GetRequiredService(); - } }