From 8abbaa46ccc28abf30d39f841a73a69df9b9a3bc Mon Sep 17 00:00:00 2001 From: Ajay Bhargav Baaskaran Date: Tue, 28 Nov 2017 12:07:56 -0800 Subject: [PATCH] Added imports tracking to TagHelper project system - #1744 --- .../FileSystemRazorProjectItem.cs | 2 +- .../RazorTemplateEngine.cs | 1 - .../DefaultRazorDocumentManager.cs | 41 ++- .../DefaultVisualStudioDocumentTracker.cs | 29 +- .../ImportChangeKind.cs | 12 + .../ImportChangedEventArgs.cs | 24 ++ .../ImportDocumentManager.cs | 16 ++ .../DefaultImportDocumentManager.cs | 255 ++++++++++++++++++ .../Editor/DefaultTextBufferProjectService.cs | 12 +- ...faultVisualStudioDocumentTrackerFactory.cs | 17 +- .../DefaultRazorDocumentManagerTest.cs | 46 ++-- .../DefaultVisualStudioDocumentTrackerTest.cs | 66 ++++- .../DefaultImportDocumentManagerTest.cs | 193 +++++++++++++ 13 files changed, 666 insertions(+), 48 deletions(-) create mode 100644 src/Microsoft.VisualStudio.Editor.Razor/ImportChangeKind.cs create mode 100644 src/Microsoft.VisualStudio.Editor.Razor/ImportChangedEventArgs.cs create mode 100644 src/Microsoft.VisualStudio.Editor.Razor/ImportDocumentManager.cs create mode 100644 src/Microsoft.VisualStudio.LanguageServices.Razor/DefaultImportDocumentManager.cs create mode 100644 test/Microsoft.VisualStudio.LanguageServices.Razor.Test/DefaultImportDocumentManagerTest.cs diff --git a/src/Microsoft.AspNetCore.Razor.Language/FileSystemRazorProjectItem.cs b/src/Microsoft.AspNetCore.Razor.Language/FileSystemRazorProjectItem.cs index 4138fbc3d8..96a45027c8 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/FileSystemRazorProjectItem.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/FileSystemRazorProjectItem.cs @@ -30,6 +30,6 @@ namespace Microsoft.AspNetCore.Razor.Language public override string PhysicalPath => File.FullName; - public override Stream Read() => File.OpenRead(); + public override Stream Read() => new FileStream(PhysicalPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete); } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Razor.Language/RazorTemplateEngine.cs b/src/Microsoft.AspNetCore.Razor.Language/RazorTemplateEngine.cs index 73bbb4d9cf..298666b56f 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/RazorTemplateEngine.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/RazorTemplateEngine.cs @@ -184,7 +184,6 @@ namespace Microsoft.AspNetCore.Razor.Language } var result = new List(); - var importProjectItems = GetImportItems(projectItem); foreach (var importItem in importProjectItems) { diff --git a/src/Microsoft.VisualStudio.Editor.Razor/DefaultRazorDocumentManager.cs b/src/Microsoft.VisualStudio.Editor.Razor/DefaultRazorDocumentManager.cs index b095d2c87a..768d2523e8 100644 --- a/src/Microsoft.VisualStudio.Editor.Razor/DefaultRazorDocumentManager.cs +++ b/src/Microsoft.VisualStudio.Editor.Razor/DefaultRazorDocumentManager.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.ComponentModel.Composition; using System.Diagnostics; +using Microsoft.CodeAnalysis.Razor; using Microsoft.VisualStudio.Text; using Microsoft.VisualStudio.Text.Editor; @@ -14,13 +15,15 @@ namespace Microsoft.VisualStudio.Editor.Razor [Export(typeof(RazorDocumentManager))] internal class DefaultRazorDocumentManager : RazorDocumentManager { + private readonly ForegroundDispatcher _foregroundDispatcher; private readonly RazorEditorFactoryService _editorFactoryService; private readonly TextBufferProjectService _projectService; [ImportingConstructor] public DefaultRazorDocumentManager( RazorEditorFactoryService editorFactoryService, - TextBufferProjectService projectService) + TextBufferProjectService projectService, + VisualStudioWorkspaceAccessor workspaceAccessor) { if (editorFactoryService == null) { @@ -32,8 +35,40 @@ namespace Microsoft.VisualStudio.Editor.Razor throw new ArgumentNullException(nameof(projectService)); } + if (workspaceAccessor == null) + { + throw new ArgumentNullException(nameof(workspaceAccessor)); + } + _editorFactoryService = editorFactoryService; _projectService = projectService; + _foregroundDispatcher = workspaceAccessor.Workspace.Services.GetRequiredService(); + } + + // This is only for testing. We want to avoid using the actual Roslyn GetService methods in unit tests. + internal DefaultRazorDocumentManager( + RazorEditorFactoryService editorFactoryService, + TextBufferProjectService projectService, + ForegroundDispatcher foregroundDispatcher) + { + if (editorFactoryService == null) + { + throw new ArgumentNullException(nameof(editorFactoryService)); + } + + if (projectService == null) + { + throw new ArgumentNullException(nameof(projectService)); + } + + if (foregroundDispatcher == null) + { + throw new ArgumentNullException(nameof(foregroundDispatcher)); + } + + _editorFactoryService = editorFactoryService; + _projectService = projectService; + _foregroundDispatcher = foregroundDispatcher; } public override void OnTextViewOpened(ITextView textView, IEnumerable subjectBuffers) @@ -48,6 +83,8 @@ namespace Microsoft.VisualStudio.Editor.Razor throw new ArgumentNullException(nameof(subjectBuffers)); } + _foregroundDispatcher.AssertForegroundThread(); + foreach (var textBuffer in subjectBuffers) { if (!textBuffer.IsRazorBuffer()) @@ -88,6 +125,8 @@ namespace Microsoft.VisualStudio.Editor.Razor throw new ArgumentNullException(nameof(subjectBuffers)); } + _foregroundDispatcher.AssertForegroundThread(); + // This means a Razor buffer has be detached from this ITextView or the ITextView is closing. Since we keep a // list of all of the open text views for each text buffer, we need to update the tracker. // diff --git a/src/Microsoft.VisualStudio.Editor.Razor/DefaultVisualStudioDocumentTracker.cs b/src/Microsoft.VisualStudio.Editor.Razor/DefaultVisualStudioDocumentTracker.cs index 07d4d4ac74..1a9758088d 100644 --- a/src/Microsoft.VisualStudio.Editor.Razor/DefaultVisualStudioDocumentTracker.cs +++ b/src/Microsoft.VisualStudio.Editor.Razor/DefaultVisualStudioDocumentTracker.cs @@ -19,6 +19,7 @@ namespace Microsoft.VisualStudio.Editor.Razor private readonly ProjectSnapshotManager _projectManager; private readonly EditorSettingsManagerInternal _editorSettingsManager; private readonly ITextBuffer _textBuffer; + private readonly ImportDocumentManager _importDocumentManager; private readonly List _textViews; private readonly Workspace _workspace; private bool _isSupportedProject; @@ -32,7 +33,8 @@ namespace Microsoft.VisualStudio.Editor.Razor ProjectSnapshotManager projectManager, EditorSettingsManagerInternal editorSettingsManager, Workspace workspace, - ITextBuffer textBuffer) + ITextBuffer textBuffer, + ImportDocumentManager importDocumentManager) { if (string.IsNullOrEmpty(filePath)) { @@ -64,11 +66,17 @@ namespace Microsoft.VisualStudio.Editor.Razor throw new ArgumentNullException(nameof(textBuffer)); } + if (importDocumentManager == null) + { + throw new ArgumentNullException(nameof(importDocumentManager)); + } + _filePath = filePath; _projectPath = projectPath; _projectManager = projectManager; _editorSettingsManager = editorSettingsManager; _textBuffer = textBuffer; + _importDocumentManager = importDocumentManager; _workspace = workspace; // For now we assume that the workspace is the always default VS workspace. _textViews = new List(); @@ -135,8 +143,11 @@ namespace Microsoft.VisualStudio.Editor.Razor public void Subscribe() { + _importDocumentManager.OnSubscribed(this); + _editorSettingsManager.Changed += EditorSettingsManager_Changed; _projectManager.Changed += ProjectManager_Changed; + _importDocumentManager.Changed += Import_Changed; _isSupportedProject = true; _project = _projectManager.GetProjectWithFilePath(_projectPath); @@ -146,8 +157,11 @@ namespace Microsoft.VisualStudio.Editor.Razor public void Unsubscribe() { + _importDocumentManager.OnUnsubscribed(this); + _projectManager.Changed -= ProjectManager_Changed; _editorSettingsManager.Changed -= EditorSettingsManager_Changed; + _importDocumentManager.Changed -= Import_Changed; // Detached from project. _isSupportedProject = false; @@ -189,5 +203,18 @@ namespace Microsoft.VisualStudio.Editor.Razor { OnContextChanged(_project, ContextChangeKind.EditorSettingsChanged); } + + // Internal for testing + internal void Import_Changed(object sender, ImportChangedEventArgs args) + { + foreach (var path in args.AssociatedDocuments) + { + if (string.Equals(_filePath, path, StringComparison.OrdinalIgnoreCase)) + { + OnContextChanged(_project, ContextChangeKind.ImportsChanged); + break; + } + } + } } } diff --git a/src/Microsoft.VisualStudio.Editor.Razor/ImportChangeKind.cs b/src/Microsoft.VisualStudio.Editor.Razor/ImportChangeKind.cs new file mode 100644 index 0000000000..ace9b769ca --- /dev/null +++ b/src/Microsoft.VisualStudio.Editor.Razor/ImportChangeKind.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.VisualStudio.Editor.Razor +{ + internal enum ImportChangeKind + { + Added, + Removed, + Changed, + } +} diff --git a/src/Microsoft.VisualStudio.Editor.Razor/ImportChangedEventArgs.cs b/src/Microsoft.VisualStudio.Editor.Razor/ImportChangedEventArgs.cs new file mode 100644 index 0000000000..b8836ca69a --- /dev/null +++ b/src/Microsoft.VisualStudio.Editor.Razor/ImportChangedEventArgs.cs @@ -0,0 +1,24 @@ +// 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; + +namespace Microsoft.VisualStudio.Editor.Razor +{ + internal class ImportChangedEventArgs : EventArgs + { + public ImportChangedEventArgs(string filePath, ImportChangeKind kind, IEnumerable associatedDocuments) + { + FilePath = filePath; + Kind = kind; + AssociatedDocuments = associatedDocuments; + } + + public string FilePath { get; } + + public ImportChangeKind Kind { get; } + + public IEnumerable AssociatedDocuments { get; } + } +} diff --git a/src/Microsoft.VisualStudio.Editor.Razor/ImportDocumentManager.cs b/src/Microsoft.VisualStudio.Editor.Razor/ImportDocumentManager.cs new file mode 100644 index 0000000000..ecfa7d911d --- /dev/null +++ b/src/Microsoft.VisualStudio.Editor.Razor/ImportDocumentManager.cs @@ -0,0 +1,16 @@ +// 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.VisualStudio.Editor.Razor +{ + internal abstract class ImportDocumentManager + { + public abstract event EventHandler Changed; + + public abstract void OnSubscribed(VisualStudioDocumentTracker tracker); + + public abstract void OnUnsubscribed(VisualStudioDocumentTracker tracker); + } +} diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/DefaultImportDocumentManager.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/DefaultImportDocumentManager.cs new file mode 100644 index 0000000000..84e564c9dc --- /dev/null +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/DefaultImportDocumentManager.cs @@ -0,0 +1,255 @@ +// 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.ComponentModel.Composition; +using System.Diagnostics; +using System.IO; +using System.Runtime.InteropServices; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.CodeAnalysis.Razor; +using Microsoft.VisualStudio.Editor.Razor; +using Microsoft.VisualStudio.Shell; +using Microsoft.VisualStudio.Shell.Interop; + +namespace Microsoft.VisualStudio.LanguageServices.Razor +{ + [System.Composition.Shared] + [Export(typeof(ImportDocumentManager))] + internal class DefaultImportDocumentManager : ImportDocumentManager + { + private const uint FileChangeFlags = (uint)(_VSFILECHANGEFLAGS.VSFILECHG_Time | _VSFILECHANGEFLAGS.VSFILECHG_Size | _VSFILECHANGEFLAGS.VSFILECHG_Del | _VSFILECHANGEFLAGS.VSFILECHG_Add); + + private readonly IVsFileChangeEx _fileChangeService; + private readonly ForegroundDispatcher _foregroundDispatcher; + private readonly ErrorReporter _errorReporter; + private readonly RazorTemplateEngineFactoryService _templateEngineFactoryService; + private readonly Dictionary _importTrackerCache; + + public override event EventHandler Changed; + + [ImportingConstructor] + public DefaultImportDocumentManager( + [Import(typeof(SVsServiceProvider))] IServiceProvider serviceProvider, + VisualStudioWorkspaceAccessor workspaceAccessor) + { + if (serviceProvider == null) + { + throw new ArgumentNullException(nameof(serviceProvider)); + } + + if (workspaceAccessor == null) + { + throw new ArgumentNullException(nameof(workspaceAccessor)); + } + + _fileChangeService = serviceProvider.GetService(typeof(SVsFileChangeEx)) as IVsFileChangeEx; + + var workspace = workspaceAccessor.Workspace; + _foregroundDispatcher = workspace.Services.GetRequiredService(); + _errorReporter = workspace.Services.GetRequiredService(); + + var razorLanguageServices = workspace.Services.GetLanguageServices(RazorLanguage.Name); + _templateEngineFactoryService = razorLanguageServices.GetRequiredService(); + + _importTrackerCache = new Dictionary(StringComparer.OrdinalIgnoreCase); + } + + // This is only used for testing. + internal DefaultImportDocumentManager( + IVsFileChangeEx fileChangeService, + RazorTemplateEngineFactoryService templateEngineFactoryService, + ForegroundDispatcher foregroundDispatcher, + ErrorReporter errorReporter) + { + _fileChangeService = fileChangeService; + _templateEngineFactoryService = templateEngineFactoryService; + _foregroundDispatcher = foregroundDispatcher; + _errorReporter = errorReporter; + + _importTrackerCache = new Dictionary(StringComparer.OrdinalIgnoreCase); + } + + public override void OnSubscribed(VisualStudioDocumentTracker tracker) + { + if (tracker == null) + { + throw new ArgumentNullException(nameof(tracker)); + } + + _foregroundDispatcher.AssertForegroundThread(); + + var imports = GetImportItems(tracker); + foreach (var import in imports) + { + var importFilePath = import.PhysicalPath; + Debug.Assert(importFilePath != null); + + if (!_importTrackerCache.TryGetValue(importFilePath, out var importTracker)) + { + // First time seeing this import. Start tracking it. + importTracker = new ImportTracker(importFilePath); + _importTrackerCache[importFilePath] = importTracker; + + StartListeningForChanges(importTracker); + } + + importTracker.AssociatedDocuments.Add(tracker.FilePath); + } + } + + public override void OnUnsubscribed(VisualStudioDocumentTracker tracker) + { + if (tracker == null) + { + throw new ArgumentNullException(nameof(tracker)); + } + + _foregroundDispatcher.AssertForegroundThread(); + + var imports = GetImportItems(tracker); + foreach (var import in imports) + { + var importFilePath = import.PhysicalPath; + Debug.Assert(importFilePath != null); + + if (_importTrackerCache.TryGetValue(importFilePath, out var importTracker)) + { + importTracker.AssociatedDocuments.Remove(tracker.FilePath); + + if (importTracker.AssociatedDocuments.Count == 0) + { + // There are no open documents that care about this import. We no longer need to track it. + StopListeningForChanges(importTracker); + _importTrackerCache.Remove(importFilePath); + } + } + } + } + + private IEnumerable GetImportItems(VisualStudioDocumentTracker tracker) + { + var projectDirectory = Path.GetDirectoryName(tracker.ProjectPath); + var templateEngine = _templateEngineFactoryService.Create(projectDirectory, _ => { }); + var imports = templateEngine.GetImportItems(tracker.FilePath); + + return imports; + } + + private void FireImportChanged(string importPath, ImportChangeKind kind) + { + _foregroundDispatcher.AssertForegroundThread(); + + var handler = Changed; + if (handler != null && _importTrackerCache.TryGetValue(importPath, out var importTracker)) + { + var args = new ImportChangedEventArgs(importPath, kind, importTracker.AssociatedDocuments); + handler(this, args); + } + } + + // internal for testing. + internal void OnFilesChanged(uint fileCount, string[] filePaths, uint[] fileChangeFlags) + { + for (var i = 0; i < fileCount; i++) + { + var kind = ImportChangeKind.Changed; + var flag = (_VSFILECHANGEFLAGS)fileChangeFlags[i]; + + if ((flag & _VSFILECHANGEFLAGS.VSFILECHG_Del) == _VSFILECHANGEFLAGS.VSFILECHG_Del) + { + kind = ImportChangeKind.Removed; + } + else if ((flag & _VSFILECHANGEFLAGS.VSFILECHG_Add) == _VSFILECHANGEFLAGS.VSFILECHG_Add) + { + kind = ImportChangeKind.Added; + } + + FireImportChanged(filePaths[i], kind); + } + } + + private void StartListeningForChanges(ImportTracker importTracker) + { + try + { + if (importTracker.FileChangeCookie == VSConstants.VSCOOKIE_NIL) + { + var hr = _fileChangeService.AdviseFileChange( + importTracker.FilePath, + FileChangeFlags, + new ImportDocumentEventSink(this, _foregroundDispatcher), + out var cookie); + + Marshal.ThrowExceptionForHR(hr); + + importTracker.FileChangeCookie = cookie; + } + } + catch (Exception exception) + { + _errorReporter.ReportError(exception); + } + } + + private void StopListeningForChanges(ImportTracker importTracker) + { + try + { + if (importTracker.FileChangeCookie != VSConstants.VSCOOKIE_NIL) + { + var hr = _fileChangeService.UnadviseFileChange(importTracker.FileChangeCookie); + Marshal.ThrowExceptionForHR(hr); + importTracker.FileChangeCookie = VSConstants.VSCOOKIE_NIL; + } + } + catch (Exception exception) + { + _errorReporter.ReportError(exception); + } + } + + private class ImportTracker + { + public ImportTracker(string filePath) + { + FilePath = filePath; + AssociatedDocuments = new HashSet(StringComparer.OrdinalIgnoreCase); + FileChangeCookie = VSConstants.VSCOOKIE_NIL; + } + + public string FilePath { get; } + + public HashSet AssociatedDocuments { get; } + + public uint FileChangeCookie { get; set; } + } + + private class ImportDocumentEventSink : IVsFileChangeEvents + { + private readonly DefaultImportDocumentManager _importDocumentManager; + private readonly ForegroundDispatcher _foregroundDispatcher; + + public ImportDocumentEventSink(DefaultImportDocumentManager importDocumentManager, ForegroundDispatcher foregroundDispatcher) + { + _importDocumentManager = importDocumentManager; + _foregroundDispatcher = foregroundDispatcher; + } + + public int FilesChanged(uint cChanges, string[] rgpszFile, uint[] rggrfChange) + { + _foregroundDispatcher.AssertForegroundThread(); + + _importDocumentManager.OnFilesChanged(cChanges, rgpszFile, rggrfChange); + + return VSConstants.S_OK; + } + + public int DirectoryChanged(string pszDirectory) + { + return VSConstants.S_OK; + } + } + } +} diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/Editor/DefaultTextBufferProjectService.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/Editor/DefaultTextBufferProjectService.cs index c60d748c3c..e0fe9a9540 100644 --- a/src/Microsoft.VisualStudio.LanguageServices.Razor/Editor/DefaultTextBufferProjectService.cs +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/Editor/DefaultTextBufferProjectService.cs @@ -3,6 +3,7 @@ using System; using System.ComponentModel.Composition; +using System.Diagnostics; using Microsoft.CodeAnalysis; using Microsoft.VisualStudio.Editor.Razor; using Microsoft.VisualStudio.Shell; @@ -71,7 +72,8 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor throw new ArgumentNullException(nameof(project)); } - var hierarchy = (IVsHierarchy)project; + var hierarchy = project as IVsHierarchy; + Debug.Assert(hierarchy != null); ErrorHandler.ThrowOnFailure(((IVsProject)hierarchy).GetMkDocument((uint)VSConstants.VSITEMID.Root, out var path), VSConstants.E_NOTIMPL); return path; @@ -84,7 +86,9 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor throw new ArgumentNullException(nameof(project)); } - var hierarchy = (IVsHierarchy)project; + var hierarchy = project as IVsHierarchy; + Debug.Assert(hierarchy != null); + try { return hierarchy.IsCapabilityMatch(DotNetCoreCapability); @@ -108,7 +112,9 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor throw new ArgumentNullException(nameof(project)); } - var hierarchy = (IVsHierarchy)project; + var hierarchy = project as IVsHierarchy; + Debug.Assert(hierarchy != null); + if (ErrorHandler.Failed(hierarchy.GetProperty((uint)VSConstants.VSITEMID.Root, (int)__VSHPROPID.VSHPROPID_Name, out var name))) { return null; diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/Editor/DefaultVisualStudioDocumentTrackerFactory.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/Editor/DefaultVisualStudioDocumentTrackerFactory.cs index c56f7d024f..9bd9570a9a 100644 --- a/src/Microsoft.VisualStudio.LanguageServices.Razor/Editor/DefaultVisualStudioDocumentTrackerFactory.cs +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/Editor/DefaultVisualStudioDocumentTrackerFactory.cs @@ -20,6 +20,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor private readonly TextBufferProjectService _projectService; private readonly ITextDocumentFactoryService _textDocumentFactory; private readonly Workspace _workspace; + private readonly ImportDocumentManager _importDocumentManager; private readonly ForegroundDispatcher _foregroundDispatcher; private readonly ProjectSnapshotManager _projectManager; private readonly EditorSettingsManagerInternal _editorSettingsManager; @@ -28,7 +29,8 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor public DefaultVisualStudioDocumentTrackerFactory( TextBufferProjectService projectService, ITextDocumentFactoryService textDocumentFactory, - [Import(typeof(VisualStudioWorkspace))] Workspace workspace) + VisualStudioWorkspaceAccessor workspaceAccessor, + ImportDocumentManager importDocumentManager) { if (projectService == null) { @@ -40,17 +42,18 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor throw new ArgumentNullException(nameof(textDocumentFactory)); } - if (workspace == null) + if (workspaceAccessor == null) { - throw new ArgumentNullException(nameof(workspace)); + throw new ArgumentNullException(nameof(workspaceAccessor)); } _projectService = projectService; _textDocumentFactory = textDocumentFactory; - _workspace = workspace; + _workspace = workspaceAccessor.Workspace; + _importDocumentManager = importDocumentManager; - _foregroundDispatcher = workspace.Services.GetRequiredService(); - var razorLanguageServices = workspace.Services.GetLanguageServices(RazorLanguage.Name); + _foregroundDispatcher = _workspace.Services.GetRequiredService(); + var razorLanguageServices = _workspace.Services.GetLanguageServices(RazorLanguage.Name); _projectManager = razorLanguageServices.GetRequiredService(); _editorSettingsManager = razorLanguageServices.GetRequiredService(); } @@ -78,7 +81,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor var projectPath = _projectService.GetProjectPath(project); - var tracker = new DefaultVisualStudioDocumentTracker(filePath, projectPath, _projectManager, _editorSettingsManager, _workspace, textBuffer); + var tracker = new DefaultVisualStudioDocumentTracker(filePath, projectPath, _projectManager, _editorSettingsManager, _workspace, textBuffer, _importDocumentManager); return tracker; } diff --git a/test/Microsoft.VisualStudio.Editor.Razor.Test/DefaultRazorDocumentManagerTest.cs b/test/Microsoft.VisualStudio.Editor.Razor.Test/DefaultRazorDocumentManagerTest.cs index d2b42f15e9..2c9db9152a 100644 --- a/test/Microsoft.VisualStudio.Editor.Razor.Test/DefaultRazorDocumentManagerTest.cs +++ b/test/Microsoft.VisualStudio.Editor.Razor.Test/DefaultRazorDocumentManagerTest.cs @@ -15,7 +15,7 @@ using Xunit; namespace Microsoft.VisualStudio.Editor.Razor { - public class DefaultRazorDocumentManagerTest + public class DefaultRazorDocumentManagerTest : ForegroundDispatcherTestBase { private IContentType RazorContentType { get; } = Mock.Of(c => c.IsOfType(RazorLanguage.ContentType) == true); @@ -29,6 +29,8 @@ namespace Microsoft.VisualStudio.Editor.Razor private EditorSettingsManagerInternal EditorSettingsManager => new DefaultEditorSettingsManagerInternal(); + private ImportDocumentManager ImportDocumentManager => Mock.Of(); + private Workspace Workspace => new AdhocWorkspace(); private TextBufferProjectService SupportedProjectService { get; } = Mock.Of( @@ -38,12 +40,12 @@ namespace Microsoft.VisualStudio.Editor.Razor private TextBufferProjectService UnsupportedProjectService { get; } = Mock.Of(s => s.IsSupportedProject(It.IsAny()) == false); - [Fact] + [ForegroundFact] public void OnTextViewOpened_ForNonRazorCoreProject_DoesNothing() { // Arrange var editorFactoryService = new Mock(MockBehavior.Strict); - var documentManager = new DefaultRazorDocumentManager(editorFactoryService.Object, UnsupportedProjectService); + var documentManager = new DefaultRazorDocumentManager(editorFactoryService.Object, UnsupportedProjectService, Dispatcher); var textView = Mock.Of(); var buffers = new Collection() { @@ -54,12 +56,12 @@ namespace Microsoft.VisualStudio.Editor.Razor documentManager.OnTextViewOpened(textView, buffers); } - [Fact] + [ForegroundFact] public void OnTextViewOpened_ForNonRazorTextBuffer_DoesNothing() { // Arrange var editorFactoryService = new Mock(MockBehavior.Strict); - var documentManager = new DefaultRazorDocumentManager(editorFactoryService.Object, SupportedProjectService); + var documentManager = new DefaultRazorDocumentManager(editorFactoryService.Object, SupportedProjectService, Dispatcher); var textView = Mock.Of(); var buffers = new Collection() { @@ -70,7 +72,7 @@ namespace Microsoft.VisualStudio.Editor.Razor documentManager.OnTextViewOpened(textView, buffers); } - [Fact] + [ForegroundFact] public void OnTextViewOpened_ForRazorTextBuffer_AddsTextViewToTracker() { // Arrange @@ -79,9 +81,9 @@ namespace Microsoft.VisualStudio.Editor.Razor { Mock.Of(b => b.ContentType == RazorContentType && b.Properties == new PropertyCollection()), }; - var documentTracker = new DefaultVisualStudioDocumentTracker(FilePath, ProjectPath, ProjectManager, EditorSettingsManager, Workspace, buffers[0]) as VisualStudioDocumentTracker; + var documentTracker = new DefaultVisualStudioDocumentTracker(FilePath, ProjectPath, ProjectManager, EditorSettingsManager, Workspace, buffers[0], ImportDocumentManager) as VisualStudioDocumentTracker; var editorFactoryService = Mock.Of(factoryService => factoryService.TryGetDocumentTracker(It.IsAny(), out documentTracker) == true); - var documentManager = new DefaultRazorDocumentManager(editorFactoryService, SupportedProjectService); + var documentManager = new DefaultRazorDocumentManager(editorFactoryService, SupportedProjectService, Dispatcher); // Act documentManager.OnTextViewOpened(textView, buffers); @@ -90,7 +92,7 @@ namespace Microsoft.VisualStudio.Editor.Razor Assert.Collection(documentTracker.TextViews, v => Assert.Same(v, textView)); } - [Fact] + [ForegroundFact] public void OnTextViewOpened_SubscribesAfterFirstTextViewOpened() { // Arrange @@ -100,9 +102,9 @@ namespace Microsoft.VisualStudio.Editor.Razor Mock.Of(b => b.ContentType == RazorContentType && b.Properties == new PropertyCollection()), Mock.Of(b => b.ContentType == NonRazorContentType && b.Properties == new PropertyCollection()), }; - var documentTracker = new DefaultVisualStudioDocumentTracker(FilePath, ProjectPath, ProjectManager, EditorSettingsManager, Workspace, buffers[0]) as VisualStudioDocumentTracker; + var documentTracker = new DefaultVisualStudioDocumentTracker(FilePath, ProjectPath, ProjectManager, EditorSettingsManager, Workspace, buffers[0], ImportDocumentManager) as VisualStudioDocumentTracker; var editorFactoryService = Mock.Of(f => f.TryGetDocumentTracker(It.IsAny(), out documentTracker) == true); - var documentManager = new DefaultRazorDocumentManager(editorFactoryService, SupportedProjectService); + var documentManager = new DefaultRazorDocumentManager(editorFactoryService, SupportedProjectService, Dispatcher); // Assert 1 Assert.False(documentTracker.IsSupportedProject); @@ -114,11 +116,11 @@ namespace Microsoft.VisualStudio.Editor.Razor Assert.True(documentTracker.IsSupportedProject); } - [Fact] + [ForegroundFact] public void OnTextViewClosed_FoNonRazorCoreProject_DoesNothing() { // Arrange - var documentManager = new DefaultRazorDocumentManager(Mock.Of(), UnsupportedProjectService); + var documentManager = new DefaultRazorDocumentManager(Mock.Of(), UnsupportedProjectService, Dispatcher); var textView = Mock.Of(); var buffers = new Collection() { @@ -132,11 +134,11 @@ namespace Microsoft.VisualStudio.Editor.Razor Assert.False(buffers[0].Properties.ContainsProperty(typeof(VisualStudioDocumentTracker))); } - [Fact] + [ForegroundFact] public void OnTextViewClosed_TextViewWithoutDocumentTracker_DoesNothing() { // Arrange - var documentManager = new DefaultRazorDocumentManager(Mock.Of(), SupportedProjectService); + var documentManager = new DefaultRazorDocumentManager(Mock.Of(), SupportedProjectService, Dispatcher); var textView = Mock.Of(); var buffers = new Collection() { @@ -150,7 +152,7 @@ namespace Microsoft.VisualStudio.Editor.Razor Assert.False(buffers[0].Properties.ContainsProperty(typeof(VisualStudioDocumentTracker))); } - [Fact] + [ForegroundFact] public void OnTextViewClosed_ForAnyTextBufferWithTracker_RemovesTextView() { // Arrange @@ -163,18 +165,18 @@ namespace Microsoft.VisualStudio.Editor.Razor }; // Preload the buffer's properties with a tracker, so it's like we've already tracked this one. - var documentTracker = new DefaultVisualStudioDocumentTracker(FilePath, ProjectPath, ProjectManager, EditorSettingsManager, Workspace, buffers[0]); + var documentTracker = new DefaultVisualStudioDocumentTracker(FilePath, ProjectPath, ProjectManager, EditorSettingsManager, Workspace, buffers[0], ImportDocumentManager); documentTracker.AddTextView(textView1); documentTracker.AddTextView(textView2); buffers[0].Properties.AddProperty(typeof(VisualStudioDocumentTracker), documentTracker); - documentTracker = new DefaultVisualStudioDocumentTracker(FilePath, ProjectPath, ProjectManager, EditorSettingsManager, Workspace, buffers[1]); + documentTracker = new DefaultVisualStudioDocumentTracker(FilePath, ProjectPath, ProjectManager, EditorSettingsManager, Workspace, buffers[1], ImportDocumentManager); documentTracker.AddTextView(textView1); documentTracker.AddTextView(textView2); buffers[1].Properties.AddProperty(typeof(VisualStudioDocumentTracker), documentTracker); var editorFactoryService = Mock.Of(); - var documentManager = new DefaultRazorDocumentManager(editorFactoryService, SupportedProjectService); + var documentManager = new DefaultRazorDocumentManager(editorFactoryService, SupportedProjectService, Dispatcher); // Act documentManager.OnTextViewClosed(textView2, buffers); @@ -187,7 +189,7 @@ namespace Microsoft.VisualStudio.Editor.Razor Assert.Collection(documentTracker.TextViews, v => Assert.Same(v, textView1)); } - [Fact] + [ForegroundFact] public void OnTextViewClosed_UnsubscribesAfterLastTextViewClosed() { // Arrange @@ -198,10 +200,10 @@ namespace Microsoft.VisualStudio.Editor.Razor Mock.Of(b => b.ContentType == RazorContentType && b.Properties == new PropertyCollection()), Mock.Of(b => b.ContentType == NonRazorContentType && b.Properties == new PropertyCollection()), }; - var documentTracker = new DefaultVisualStudioDocumentTracker(FilePath, ProjectPath, ProjectManager, EditorSettingsManager, Workspace, buffers[0]); + var documentTracker = new DefaultVisualStudioDocumentTracker(FilePath, ProjectPath, ProjectManager, EditorSettingsManager, Workspace, buffers[0], ImportDocumentManager); buffers[0].Properties.AddProperty(typeof(VisualStudioDocumentTracker), documentTracker); var editorFactoryService = Mock.Of(); - var documentManager = new DefaultRazorDocumentManager(editorFactoryService, SupportedProjectService); + var documentManager = new DefaultRazorDocumentManager(editorFactoryService, SupportedProjectService, Dispatcher); // Populate the text views documentTracker.Subscribe(); diff --git a/test/Microsoft.VisualStudio.Editor.Razor.Test/DefaultVisualStudioDocumentTrackerTest.cs b/test/Microsoft.VisualStudio.Editor.Razor.Test/DefaultVisualStudioDocumentTrackerTest.cs index ac9c752069..6c2a7c0be2 100644 --- a/test/Microsoft.VisualStudio.Editor.Razor.Test/DefaultVisualStudioDocumentTrackerTest.cs +++ b/test/Microsoft.VisualStudio.Editor.Razor.Test/DefaultVisualStudioDocumentTrackerTest.cs @@ -1,6 +1,7 @@ // 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; @@ -30,11 +31,13 @@ namespace Microsoft.VisualStudio.Editor.Razor private Workspace Workspace => new AdhocWorkspace(); + private ImportDocumentManager ImportDocumentManager => Mock.Of(); + [Fact] public void EditorSettingsManager_Changed_TriggersContextChanged() { // Arrange - var documentTracker = new DefaultVisualStudioDocumentTracker(FilePath, ProjectPath, ProjectManager, EditorSettingsManager, Workspace, TextBuffer); + var documentTracker = new DefaultVisualStudioDocumentTracker(FilePath, ProjectPath, ProjectManager, EditorSettingsManager, Workspace, TextBuffer, ImportDocumentManager); var called = false; documentTracker.ContextChanged += (sender, args) => { @@ -54,7 +57,7 @@ namespace Microsoft.VisualStudio.Editor.Razor public void ProjectManager_Changed_ProjectChanged_TriggersContextChanged() { // Arrange - var documentTracker = new DefaultVisualStudioDocumentTracker(FilePath, ProjectPath, ProjectManager, EditorSettingsManager, Workspace, TextBuffer); + var documentTracker = new DefaultVisualStudioDocumentTracker(FilePath, ProjectPath, ProjectManager, EditorSettingsManager, Workspace, TextBuffer, ImportDocumentManager); var project = new AdhocWorkspace().AddProject(ProjectInfo.Create(ProjectId.CreateNewId(), new VersionStamp(), "Test1", "TestAssembly", LanguageNames.CSharp, filePath: "C:/Some/Path/TestProject.csproj")); var projectSnapshot = new DefaultProjectSnapshot(project); @@ -78,7 +81,7 @@ namespace Microsoft.VisualStudio.Editor.Razor public void ProjectManager_Changed_TagHelpersChanged_TriggersContextChanged() { // Arrange - var documentTracker = new DefaultVisualStudioDocumentTracker(FilePath, ProjectPath, ProjectManager, EditorSettingsManager, Workspace, TextBuffer); + var documentTracker = new DefaultVisualStudioDocumentTracker(FilePath, ProjectPath, ProjectManager, EditorSettingsManager, Workspace, TextBuffer, ImportDocumentManager); var project = new AdhocWorkspace().AddProject(ProjectInfo.Create(ProjectId.CreateNewId(), new VersionStamp(), "Test1", "TestAssembly", LanguageNames.CSharp, filePath: "C:/Some/Path/TestProject.csproj")); var projectSnapshot = new DefaultProjectSnapshot(project); @@ -102,7 +105,7 @@ namespace Microsoft.VisualStudio.Editor.Razor public void ProjectManager_Changed_IgnoresUnknownProject() { // Arrange - var documentTracker = new DefaultVisualStudioDocumentTracker(FilePath, ProjectPath, ProjectManager, EditorSettingsManager, Workspace, TextBuffer); + var documentTracker = new DefaultVisualStudioDocumentTracker(FilePath, ProjectPath, ProjectManager, EditorSettingsManager, Workspace, TextBuffer, ImportDocumentManager); var project = new AdhocWorkspace().AddProject(ProjectInfo.Create(ProjectId.CreateNewId(), new VersionStamp(), "Test1", "TestAssembly", LanguageNames.CSharp, filePath: "C:/Some/Other/Path/TestProject.csproj")); var projectSnapshot = new DefaultProjectSnapshot(project); @@ -121,11 +124,50 @@ namespace Microsoft.VisualStudio.Editor.Razor Assert.False(called); } + [Fact] + public void Import_Changed_ImportAssociatedWithDocument_TriggersContextChanged() + { + // Arrange + var documentTracker = new DefaultVisualStudioDocumentTracker(FilePath, ProjectPath, ProjectManager, EditorSettingsManager, Workspace, TextBuffer, ImportDocumentManager); + + var called = false; + documentTracker.ContextChanged += (sender, args) => + { + Assert.Equal(ContextChangeKind.ImportsChanged, args.Kind); + called = true; + }; + + var importChangedArgs = new ImportChangedEventArgs("path/to/import", ImportChangeKind.Changed, new[] { FilePath }); + + // Act + documentTracker.Import_Changed(null, importChangedArgs); + + // Assert + Assert.True(called); + } + + [Fact] + public void Import_Changed_UnrelatedImport_DoesNothing() + { + // Arrange + var documentTracker = new DefaultVisualStudioDocumentTracker(FilePath, ProjectPath, ProjectManager, EditorSettingsManager, Workspace, TextBuffer, ImportDocumentManager); + + documentTracker.ContextChanged += (sender, args) => + { + throw new InvalidOperationException(); + }; + + var importChangedArgs = new ImportChangedEventArgs("path/to/import", ImportChangeKind.Changed, new[] { "path/to/differentfile" }); + + // Act & Assert (Does not throw) + documentTracker.Import_Changed(null, importChangedArgs); + } + [Fact] public void Subscribe_SetsSupportedProjectAndTriggersContextChanged() { // Arrange - var documentTracker = new DefaultVisualStudioDocumentTracker(FilePath, ProjectPath, ProjectManager, EditorSettingsManager, Workspace, TextBuffer); + var documentTracker = new DefaultVisualStudioDocumentTracker(FilePath, ProjectPath, ProjectManager, EditorSettingsManager, Workspace, TextBuffer, ImportDocumentManager); var called = false; documentTracker.ContextChanged += (sender, args) => { @@ -145,7 +187,7 @@ namespace Microsoft.VisualStudio.Editor.Razor public void Unsubscribe_ResetsSupportedProjectAndTriggersContextChanged() { // Arrange - var documentTracker = new DefaultVisualStudioDocumentTracker(FilePath, ProjectPath, ProjectManager, EditorSettingsManager, Workspace, TextBuffer); + var documentTracker = new DefaultVisualStudioDocumentTracker(FilePath, ProjectPath, ProjectManager, EditorSettingsManager, Workspace, TextBuffer, ImportDocumentManager); // Subscribe once to set supported project documentTracker.Subscribe(); @@ -169,7 +211,7 @@ namespace Microsoft.VisualStudio.Editor.Razor public void AddTextView_AddsToTextViewCollection() { // Arrange - var documentTracker = new DefaultVisualStudioDocumentTracker(FilePath, ProjectPath, ProjectManager, EditorSettingsManager, Workspace, TextBuffer); + var documentTracker = new DefaultVisualStudioDocumentTracker(FilePath, ProjectPath, ProjectManager, EditorSettingsManager, Workspace, TextBuffer, ImportDocumentManager); var textView = Mock.Of(); // Act @@ -183,7 +225,7 @@ namespace Microsoft.VisualStudio.Editor.Razor public void AddTextView_DoesNotAddDuplicateTextViews() { // Arrange - var documentTracker = new DefaultVisualStudioDocumentTracker(FilePath, ProjectPath, ProjectManager, EditorSettingsManager, Workspace, TextBuffer); + var documentTracker = new DefaultVisualStudioDocumentTracker(FilePath, ProjectPath, ProjectManager, EditorSettingsManager, Workspace, TextBuffer, ImportDocumentManager); var textView = Mock.Of(); // Act @@ -198,7 +240,7 @@ namespace Microsoft.VisualStudio.Editor.Razor public void AddTextView_AddsMultipleTextViewsToCollection() { // Arrange - var documentTracker = new DefaultVisualStudioDocumentTracker(FilePath, ProjectPath, ProjectManager, EditorSettingsManager, Workspace, TextBuffer); + var documentTracker = new DefaultVisualStudioDocumentTracker(FilePath, ProjectPath, ProjectManager, EditorSettingsManager, Workspace, TextBuffer, ImportDocumentManager); var textView1 = Mock.Of(); var textView2 = Mock.Of(); @@ -217,7 +259,7 @@ namespace Microsoft.VisualStudio.Editor.Razor public void RemoveTextView_RemovesTextViewFromCollection_SingleItem() { // Arrange - var documentTracker = new DefaultVisualStudioDocumentTracker(FilePath, ProjectPath, ProjectManager, EditorSettingsManager, Workspace, TextBuffer); + var documentTracker = new DefaultVisualStudioDocumentTracker(FilePath, ProjectPath, ProjectManager, EditorSettingsManager, Workspace, TextBuffer, ImportDocumentManager); var textView = Mock.Of(); documentTracker.AddTextView(textView); @@ -232,7 +274,7 @@ namespace Microsoft.VisualStudio.Editor.Razor public void RemoveTextView_RemovesTextViewFromCollection_MultipleItems() { // Arrange - var documentTracker = new DefaultVisualStudioDocumentTracker(FilePath, ProjectPath, ProjectManager, EditorSettingsManager, Workspace, TextBuffer); + var documentTracker = new DefaultVisualStudioDocumentTracker(FilePath, ProjectPath, ProjectManager, EditorSettingsManager, Workspace, TextBuffer, ImportDocumentManager); var textView1 = Mock.Of(); var textView2 = Mock.Of(); var textView3 = Mock.Of(); @@ -254,7 +296,7 @@ namespace Microsoft.VisualStudio.Editor.Razor public void RemoveTextView_NoopsWhenRemovingTextViewNotInCollection() { // Arrange - var documentTracker = new DefaultVisualStudioDocumentTracker(FilePath, ProjectPath, ProjectManager, EditorSettingsManager, Workspace, TextBuffer); + var documentTracker = new DefaultVisualStudioDocumentTracker(FilePath, ProjectPath, ProjectManager, EditorSettingsManager, Workspace, TextBuffer, ImportDocumentManager); var textView1 = Mock.Of(); documentTracker.AddTextView(textView1); var textView2 = Mock.Of(); diff --git a/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/DefaultImportDocumentManagerTest.cs b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/DefaultImportDocumentManagerTest.cs new file mode 100644 index 0000000000..1802ffde93 --- /dev/null +++ b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/DefaultImportDocumentManagerTest.cs @@ -0,0 +1,193 @@ +// 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.Razor; +using Microsoft.CodeAnalysis.Razor.ProjectSystem; +using Microsoft.VisualStudio.Editor.Razor; +using Microsoft.VisualStudio.Shell.Interop; +using Moq; +using Xunit; + +namespace Microsoft.VisualStudio.LanguageServices.Razor +{ + public class DefaultImportDocumentManagerTest : ForegroundDispatcherTestBase + { + [ForegroundFact] + public void OnSubscribed_StartTrackingImport() + { + // Arrange + var filePath = "C:\\path\\to\\project\\Views\\Home\\file.cshtml"; + var projectPath = "C:\\path\\to\\project\\project.csproj"; + var tracker = Mock.Of(t => t.FilePath == filePath && t.ProjectPath == projectPath); + var templateEngineFactoryService = GetTemplateEngineFactoryService(); + + uint cookie; + var fileChangeService = new Mock(MockBehavior.Strict); + fileChangeService + .Setup(f => f.AdviseFileChange("C:\\path\\to\\project\\Views\\Home\\_ViewImports.cshtml", It.IsAny(), It.IsAny(), out cookie)) + .Returns(VSConstants.S_OK) + .Verifiable(); + fileChangeService + .Setup(f => f.AdviseFileChange("C:\\path\\to\\project\\Views\\_ViewImports.cshtml", It.IsAny(), It.IsAny(), out cookie)) + .Returns(VSConstants.S_OK) + .Verifiable(); + fileChangeService + .Setup(f => f.AdviseFileChange("C:\\path\\to\\project\\_ViewImports.cshtml", It.IsAny(), It.IsAny(), out cookie)) + .Returns(VSConstants.S_OK) + .Verifiable(); + + var manager = new DefaultImportDocumentManager(fileChangeService.Object, templateEngineFactoryService, Dispatcher, new DefaultErrorReporter()); + + // Act + manager.OnSubscribed(tracker); + + // Assert + fileChangeService.Verify(); + } + + [ForegroundFact] + public void OnSubscribed_AlreadyTrackingImport_DoesNothing() + { + // Arrange + var filePath = "C:\\path\\to\\project\\file.cshtml"; + var projectPath = "C:\\path\\to\\project\\project.csproj"; + var tracker = Mock.Of(t => t.FilePath == filePath && t.ProjectPath == projectPath); + var templateEngineFactoryService = GetTemplateEngineFactoryService(); + + uint cookie; + var callCount = 0; + var fileChangeService = new Mock(); + fileChangeService + .Setup(f => f.AdviseFileChange(It.IsAny(), It.IsAny(), It.IsAny(), out cookie)) + .Returns(VSConstants.S_OK) + .Callback(() => callCount++); + + var manager = new DefaultImportDocumentManager(fileChangeService.Object, templateEngineFactoryService, Dispatcher, new DefaultErrorReporter()); + manager.OnSubscribed(tracker); // Start tracking the import. + + var anotherFilePath = "C:\\path\\to\\project\\anotherFile.cshtml"; + var anotherTracker = Mock.Of(t => t.FilePath == anotherFilePath && t.ProjectPath == projectPath); + + // Act + manager.OnSubscribed(anotherTracker); + + // Assert + Assert.Equal(1, callCount); + } + + [ForegroundFact] + public void OnUnsubscribed_StopsTrackingImport() + { + // Arrange + var filePath = "C:\\path\\to\\project\\file.cshtml"; + var projectPath = "C:\\path\\to\\project\\project.csproj"; + var tracker = Mock.Of(t => t.FilePath == filePath && t.ProjectPath == projectPath); + var templateEngineFactoryService = GetTemplateEngineFactoryService(); + + uint cookie = 100; + var fileChangeService = new Mock(MockBehavior.Strict); + fileChangeService + .Setup(f => f.AdviseFileChange("C:\\path\\to\\project\\_ViewImports.cshtml", It.IsAny(), It.IsAny(), out cookie)) + .Returns(VSConstants.S_OK) + .Verifiable(); + fileChangeService + .Setup(f => f.UnadviseFileChange(cookie)) + .Returns(VSConstants.S_OK) + .Verifiable(); + + var manager = new DefaultImportDocumentManager(fileChangeService.Object, templateEngineFactoryService, Dispatcher, new DefaultErrorReporter()); + manager.OnSubscribed(tracker); // Start tracking the import. + + // Act + manager.OnUnsubscribed(tracker); + + // Assert + fileChangeService.Verify(); + } + + [ForegroundFact] + public void OnUnsubscribed_AnotherDocumentTrackingImport_DoesNotStopTrackingImport() + { + // Arrange + var filePath = "C:\\path\\to\\project\\file.cshtml"; + var projectPath = "C:\\path\\to\\project\\project.csproj"; + var tracker = Mock.Of(t => t.FilePath == filePath && t.ProjectPath == projectPath); + var templateEngineFactoryService = GetTemplateEngineFactoryService(); + + uint cookie; + var fileChangeService = new Mock(); + fileChangeService + .Setup(f => f.AdviseFileChange(It.IsAny(), It.IsAny(), It.IsAny(), out cookie)) + .Returns(VSConstants.S_OK); + fileChangeService + .Setup(f => f.UnadviseFileChange(It.IsAny())) + .Returns(VSConstants.S_OK) + .Callback(() => throw new InvalidOperationException()); + + var manager = new DefaultImportDocumentManager(fileChangeService.Object, templateEngineFactoryService, Dispatcher, new DefaultErrorReporter()); + manager.OnSubscribed(tracker); // Starts tracking import for the first document. + + var anotherFilePath = "C:\\path\\to\\project\\anotherFile.cshtml"; + var anotherTracker = Mock.Of(t => t.FilePath == anotherFilePath && t.ProjectPath == projectPath); + manager.OnSubscribed(anotherTracker); // Starts tracking import for the second document. + + // Act & Assert (Does not throw) + manager.OnUnsubscribed(tracker); + } + + [ForegroundTheory] + [InlineData((uint)_VSFILECHANGEFLAGS.VSFILECHG_Size, (int)ImportChangeKind.Changed)] + [InlineData((uint)_VSFILECHANGEFLAGS.VSFILECHG_Time, (int)ImportChangeKind.Changed)] + [InlineData((uint)_VSFILECHANGEFLAGS.VSFILECHG_Add, (int)ImportChangeKind.Added)] + [InlineData((uint)_VSFILECHANGEFLAGS.VSFILECHG_Del, (int)ImportChangeKind.Removed)] + public void OnFilesChanged_WithSpecificFlags_InvokesChangedHandler_WithExpectedArguments(uint fileChangeFlag, int expectedKind) + { + // Arrange + var filePath = "C:\\path\\to\\project\\file.cshtml"; + var projectPath = "C:\\path\\to\\project\\project.csproj"; + var tracker = Mock.Of(t => t.FilePath == filePath && t.ProjectPath == projectPath); + var templateEngineFactoryService = GetTemplateEngineFactoryService(); + + var anotherFilePath = "C:\\path\\to\\project\\anotherFile.cshtml"; + var anotherTracker = Mock.Of(t => t.FilePath == anotherFilePath && t.ProjectPath == projectPath); + + uint cookie; + var fileChangeService = new Mock(); + fileChangeService + .Setup(f => f.AdviseFileChange(It.IsAny(), It.IsAny(), It.IsAny(), out cookie)) + .Returns(VSConstants.S_OK); + var manager = new DefaultImportDocumentManager(fileChangeService.Object, templateEngineFactoryService, Dispatcher, new DefaultErrorReporter()); + manager.OnSubscribed(tracker); + manager.OnSubscribed(anotherTracker); + + var called = false; + manager.Changed += (sender, args) => + { + called = true; + Assert.Same(sender, manager); + Assert.Equal("C:\\path\\to\\project\\_ViewImports.cshtml", args.FilePath); + Assert.Equal((ImportChangeKind)expectedKind, args.Kind); + Assert.Collection( + args.AssociatedDocuments, + f => Assert.Equal(filePath, f), + f => Assert.Equal(anotherFilePath, f)); + }; + + // Act + manager.OnFilesChanged(fileCount: 1, filePaths: new[] { "C:\\path\\to\\project\\_ViewImports.cshtml" }, fileChangeFlags: new[] { fileChangeFlag }); + + // Assert + Assert.True(called); + } + + private RazorTemplateEngineFactoryService GetTemplateEngineFactoryService() + { + var projectManager = new Mock(); + projectManager.Setup(p => p.Projects).Returns(Array.Empty()); + + var service = new DefaultTemplateEngineFactoryService(projectManager.Object); + return service; + } + } +}