Added necessary infrastructure for Tag helper project system imports

tracking
This commit is contained in:
Ajay Bhargav Baaskaran 2017-11-20 19:25:46 -08:00
parent ece080466b
commit 90120f6a3b
12 changed files with 509 additions and 341 deletions

View File

@ -8,5 +8,6 @@ namespace Microsoft.VisualStudio.Editor.Razor
ProjectChanged,
EditorSettingsChanged,
TagHelpersChanged,
ImportsChanged,
}
}

View File

@ -0,0 +1,134 @@
// 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 Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Editor;
namespace Microsoft.VisualStudio.Editor.Razor
{
[System.Composition.Shared]
[Export(typeof(RazorDocumentManager))]
internal class DefaultRazorDocumentManager : RazorDocumentManager
{
private readonly RazorEditorFactoryService _editorFactoryService;
private readonly TextBufferProjectService _projectService;
[ImportingConstructor]
public DefaultRazorDocumentManager(
RazorEditorFactoryService editorFactoryService,
TextBufferProjectService projectService)
{
if (editorFactoryService == null)
{
throw new ArgumentNullException(nameof(editorFactoryService));
}
if (projectService == null)
{
throw new ArgumentNullException(nameof(projectService));
}
_editorFactoryService = editorFactoryService;
_projectService = projectService;
}
public override void OnTextViewOpened(ITextView textView, IList<ITextBuffer> subjectBuffers)
{
if (textView == null)
{
throw new ArgumentNullException(nameof(textView));
}
if (subjectBuffers == null)
{
throw new ArgumentNullException(nameof(subjectBuffers));
}
for (var i = 0; i < subjectBuffers.Count; i++)
{
var textBuffer = subjectBuffers[i];
if (!textBuffer.IsRazorBuffer())
{
continue;
}
if (!IsSupportedProject(textBuffer))
{
return;
}
if (!_editorFactoryService.TryGetDocumentTracker(textBuffer, out var documentTracker) ||
!(documentTracker is DefaultVisualStudioDocumentTracker tracker))
{
Debug.Fail("Tracker should always be available given our expectations of the VS workflow.");
return;
}
tracker.AddTextView(textView);
if (documentTracker.TextViews.Count == 1)
{
tracker.Subscribe();
}
}
}
public override void OnTextViewClosed(ITextView textView, IList<ITextBuffer> subjectBuffers)
{
if (textView == null)
{
throw new ArgumentNullException(nameof(textView));
}
if (subjectBuffers == null)
{
throw new ArgumentNullException(nameof(subjectBuffers));
}
// 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.
//
// Notice that this method is called *after* changes are applied to the text buffer(s). We need to check every
// one of them for a tracker because the content type could have changed.
for (var i = 0; i < subjectBuffers.Count; i++)
{
var textBuffer = subjectBuffers[i];
DefaultVisualStudioDocumentTracker documentTracker;
if (textBuffer.Properties.TryGetProperty(typeof(VisualStudioDocumentTracker), out documentTracker))
{
documentTracker.RemoveTextView(textView);
if (documentTracker.TextViews.Count == 0)
{
documentTracker.Unsubscribe();
}
}
}
}
private bool IsSupportedProject(ITextBuffer textBuffer)
{
// Fundamentally we have a Razor half of the world 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 if the buffer is something other than .cshtml.
object project = null;
var isSupportedProject = false;
// 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.
if ((project = _projectService.GetHostProject(textBuffer)) != null)
{
isSupportedProject = _projectService.IsSupportedProject(project);
}
return isSupportedProject;
}
}
}

View File

@ -5,35 +5,31 @@ using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Razor;
using Microsoft.CodeAnalysis.Razor.Editor;
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
using Microsoft.VisualStudio.Editor.Razor;
using Microsoft.VisualStudio.Shell.Interop;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Editor;
namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor
namespace Microsoft.VisualStudio.Editor.Razor
{
internal class DefaultVisualStudioDocumentTracker : VisualStudioDocumentTracker
{
private readonly string _filePath;
private readonly string _projectPath;
private readonly ProjectSnapshotManager _projectManager;
private readonly EditorSettingsManagerInternal _editorSettingsManager;
private readonly TextBufferProjectService _projectService;
private readonly ITextBuffer _textBuffer;
private readonly List<ITextView> _textViews;
private readonly Workspace _workspace;
private bool _isSupportedProject;
private ProjectSnapshot _project;
private string _projectPath;
public override event EventHandler<ContextChangeEventArgs> ContextChanged;
public DefaultVisualStudioDocumentTracker(
string filePath,
string projectPath,
ProjectSnapshotManager projectManager,
TextBufferProjectService projectService,
EditorSettingsManagerInternal editorSettingsManager,
Workspace workspace,
ITextBuffer textBuffer)
@ -43,16 +39,16 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor
throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(filePath));
}
if (projectPath == null)
{
throw new ArgumentNullException(nameof(projectPath));
}
if (projectManager == null)
{
throw new ArgumentNullException(nameof(projectManager));
}
if (projectService == null)
{
throw new ArgumentNullException(nameof(projectService));
}
if (editorSettingsManager == null)
{
throw new ArgumentNullException(nameof(editorSettingsManager));
@ -69,8 +65,8 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor
}
_filePath = filePath;
_projectPath = projectPath;
_projectManager = projectManager;
_projectService = projectService;
_editorSettingsManager = editorSettingsManager;
_textBuffer = textBuffer;
_workspace = workspace; // For now we assume that the workspace is the always default VS workspace.
@ -108,11 +104,6 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor
if (!_textViews.Contains(textView))
{
_textViews.Add(textView);
if (_textViews.Count == 1)
{
Subscribe();
}
}
}
@ -126,11 +117,6 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor
if (_textViews.Contains(textView))
{
_textViews.Remove(textView);
if (_textViews.Count == 0)
{
Unsubscribe();
}
}
}
@ -147,42 +133,18 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor
return null;
}
private void Subscribe()
public void Subscribe()
{
// 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 if the buffer is something other than .cshtml.
IVsHierarchy hierarchy = null;
string projectPath = null;
var isSupportedProject = false;
if (_textBuffer.ContentType.IsOfType(RazorLanguage.ContentType) &&
// 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.
(hierarchy = _projectService.GetHierarchy(_textBuffer)) != null)
{
projectPath = _projectService.GetProjectPath(hierarchy);
isSupportedProject = _projectService.IsSupportedProject(hierarchy);
}
if (!isSupportedProject || projectPath == null)
{
return;
}
_isSupportedProject = isSupportedProject;
_projectPath = projectPath;
_project = _projectManager.GetProjectWithFilePath(projectPath);
_projectManager.Changed += ProjectManager_Changed;
_editorSettingsManager.Changed += EditorSettingsManager_Changed;
_projectManager.Changed += ProjectManager_Changed;
_isSupportedProject = true;
_project = _projectManager.GetProjectWithFilePath(_projectPath);
OnContextChanged(_project, ContextChangeKind.ProjectChanged);
}
private void Unsubscribe()
public void Unsubscribe()
{
_projectManager.Changed -= ProjectManager_Changed;
_editorSettingsManager.Changed -= EditorSettingsManager_Changed;
@ -190,6 +152,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor
// Detached from project.
_isSupportedProject = false;
_project = null;
OnContextChanged(project: null, kind: ContextChangeKind.ProjectChanged);
}

View File

@ -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.Collections.Generic;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Editor;
namespace Microsoft.VisualStudio.Editor.Razor
{
internal abstract class RazorDocumentManager
{
public abstract void OnTextViewOpened(ITextView textView, IList<ITextBuffer> subjectBuffers);
public abstract void OnTextViewClosed(ITextView textView, IList<ITextBuffer> subjectBuffers);
}
}

View File

@ -1,15 +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 Microsoft.VisualStudio.Shell.Interop;
using Microsoft.VisualStudio.Text;
namespace Microsoft.VisualStudio.LanguageServices.Razor
namespace Microsoft.VisualStudio.Editor.Razor
{
internal abstract class TextBufferProjectService
{
public abstract IVsHierarchy GetHierarchy(ITextBuffer textBuffer);
public abstract object GetHostProject(ITextBuffer textBuffer);
public abstract bool IsSupportedProject(IVsHierarchy hierarchy);
public abstract bool IsSupportedProject(object project);
public abstract string GetProjectPath(object project);
}
}

View File

@ -4,6 +4,7 @@
using System;
using System.ComponentModel.Composition;
using Microsoft.CodeAnalysis;
using Microsoft.VisualStudio.Editor.Razor;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Shell.Interop;
using Microsoft.VisualStudio.Text;
@ -41,7 +42,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor
_documentTable = new RunningDocumentTable(services);
}
public override IVsHierarchy GetHierarchy(ITextBuffer textBuffer)
public override object GetHostProject(ITextBuffer textBuffer)
{
if (textBuffer == null)
{
@ -63,24 +64,27 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor
return hierarchy;
}
public override string GetProjectPath(IVsHierarchy hierarchy)
public override string GetProjectPath(object project)
{
if (hierarchy == null)
if (project == null)
{
throw new ArgumentNullException(nameof(hierarchy));
throw new ArgumentNullException(nameof(project));
}
var hierarchy = (IVsHierarchy)project;
ErrorHandler.ThrowOnFailure(((IVsProject)hierarchy).GetMkDocument((uint)VSConstants.VSITEMID.Root, out var path), VSConstants.E_NOTIMPL);
return path;
}
public override bool IsSupportedProject(IVsHierarchy hierarchy)
public override bool IsSupportedProject(object project)
{
if (hierarchy == null)
if (project == null)
{
throw new ArgumentNullException(nameof(hierarchy));
throw new ArgumentNullException(nameof(project));
}
var hierarchy = (IVsHierarchy)project;
try
{
return hierarchy.IsCapabilityMatch(DotNetCoreCapability);

View File

@ -69,7 +69,16 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor
}
var filePath = textDocument.FilePath;
var tracker = new DefaultVisualStudioDocumentTracker(filePath, _projectManager, _projectService, _editorSettingsManager, _workspace, textBuffer);
var project = _projectService.GetHostProject(textBuffer);
if (project == null)
{
Debug.Fail("Text buffer should belong to a project.");
return null;
}
var projectPath = _projectService.GetProjectPath(project);
var tracker = new DefaultVisualStudioDocumentTracker(filePath, projectPath, _projectManager, _editorSettingsManager, _workspace, textBuffer);
return tracker;
}

View File

@ -4,12 +4,9 @@
using System;
using System.Collections.ObjectModel;
using System.ComponentModel.Composition;
using System.Diagnostics;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Razor;
using Microsoft.VisualStudio.Editor.Razor;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Shell.Interop;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio.Utilities;
@ -22,69 +19,53 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor
internal class RazorTextViewConnectionListener : IWpfTextViewConnectionListener
{
private readonly ForegroundDispatcher _foregroundDispatcher;
private readonly TextBufferProjectService _projectService;
private readonly RazorEditorFactoryService _editorFactoryService;
private readonly Workspace _workspace;
private readonly RazorDocumentManager _documentManager;
[ImportingConstructor]
public RazorTextViewConnectionListener(
TextBufferProjectService projectService,
RazorEditorFactoryService editorFactoryService,
[Import(typeof(VisualStudioWorkspace))] Workspace workspace)
[Import(typeof(VisualStudioWorkspace))] Workspace workspace,
RazorDocumentManager documentManager)
{
if (projectService == null)
{
throw new ArgumentNullException(nameof(projectService));
}
if (editorFactoryService == null)
{
throw new ArgumentNullException(nameof(editorFactoryService));
}
if (workspace == null)
{
throw new ArgumentNullException(nameof(workspace));
}
_projectService = projectService;
_editorFactoryService = editorFactoryService;
_workspace = workspace;
if (documentManager == null)
{
throw new ArgumentNullException(nameof(documentManager));
}
_workspace = workspace;
_documentManager = documentManager;
_foregroundDispatcher = workspace.Services.GetRequiredService<ForegroundDispatcher>();
}
// This is only for testing. We want to avoid using the actual Roslyn GetService methods in unit tests.
internal RazorTextViewConnectionListener(
ForegroundDispatcher foregroundDispatcher,
TextBufferProjectService projectService,
RazorEditorFactoryService editorFactoryService,
[Import(typeof(VisualStudioWorkspace))] Workspace workspace)
Workspace workspace,
RazorDocumentManager documentManager)
{
if (foregroundDispatcher == null)
{
throw new ArgumentNullException(nameof(foregroundDispatcher));
}
if (projectService == null)
{
throw new ArgumentNullException(nameof(projectService));
}
if (editorFactoryService == null)
{
throw new ArgumentNullException(nameof(editorFactoryService));
}
if (workspace == null)
{
throw new ArgumentNullException(nameof(workspace));
}
if (documentManager == null)
{
throw new ArgumentNullException(nameof(documentManager));
}
_foregroundDispatcher = foregroundDispatcher;
_projectService = projectService;
_editorFactoryService = editorFactoryService;
_workspace = workspace;
_documentManager = documentManager;
}
public Workspace Workspace => _workspace;
@ -103,29 +84,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor
_foregroundDispatcher.AssertForegroundThread();
for (var i = 0; i < subjectBuffers.Count; i++)
{
var textBuffer = subjectBuffers[i];
if (!textBuffer.IsRazorBuffer())
{
continue;
}
var hierarchy = _projectService.GetHierarchy(textBuffer);
if (!_projectService.IsSupportedProject(hierarchy))
{
return;
}
if (!_editorFactoryService.TryGetDocumentTracker(textBuffer, out var documentTracker) ||
!(documentTracker is DefaultVisualStudioDocumentTracker tracker))
{
Debug.Fail("Tracker should always be available given our expectations of the VS workflow.");
return;
}
tracker.AddTextView(textView);
}
_documentManager.OnTextViewOpened(textView, subjectBuffers);
}
public void SubjectBuffersDisconnected(IWpfTextView textView, ConnectionReason reason, Collection<ITextBuffer> subjectBuffers)
@ -142,21 +101,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor
_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.
//
// Notice that this method is called *after* changes are applied to the text buffer(s). We need to check every
// one of them for a tracker because the content type could have changed.
for (var i = 0; i < subjectBuffers.Count; i++)
{
var textBuffer = subjectBuffers[i];
DefaultVisualStudioDocumentTracker documentTracker;
if (textBuffer.Properties.TryGetProperty(typeof(VisualStudioDocumentTracker), out documentTracker))
{
documentTracker.RemoveTextView(textView);
}
}
_documentManager.OnTextViewClosed(textView, subjectBuffers);
}
}
}

View File

@ -1,17 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.VisualStudio.Shell.Interop;
using Microsoft.VisualStudio.Text;
namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor
{
internal abstract class TextBufferProjectService
{
public abstract IVsHierarchy GetHierarchy(ITextBuffer textBuffer);
public abstract bool IsSupportedProject(IVsHierarchy hierarchy);
public abstract string GetProjectPath(IVsHierarchy hierarchy);
}
}

View File

@ -0,0 +1,224 @@
// 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.Collections.ObjectModel;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Razor;
using Microsoft.CodeAnalysis.Razor.Editor;
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio.Utilities;
using Moq;
using Xunit;
namespace Microsoft.VisualStudio.Editor.Razor
{
public class DefaultRazorDocumentManagerTest
{
private IContentType RazorContentType { get; } = Mock.Of<IContentType>(c => c.IsOfType(RazorLanguage.ContentType) == true);
private IContentType NonRazorContentType { get; } = Mock.Of<IContentType>(c => c.IsOfType(It.IsAny<string>()) == false);
private string FilePath => "C:/Some/Path/TestDocumentTracker.cshtml";
private string ProjectPath => "C:/Some/Path/TestProject.csproj";
private ProjectSnapshotManager ProjectManager => Mock.Of<ProjectSnapshotManager>(p => p.Projects == new List<ProjectSnapshot>());
private EditorSettingsManagerInternal EditorSettingsManager => new DefaultEditorSettingsManagerInternal();
private Workspace Workspace => new AdhocWorkspace();
private TextBufferProjectService SupportedProjectService { get; } = Mock.Of<TextBufferProjectService>(
s => s.GetHostProject(It.IsAny<ITextBuffer>()) == Mock.Of<object>() &&
s.IsSupportedProject(It.IsAny<object>()) == true &&
s.GetProjectPath(It.IsAny<object>()) == "C:/Some/Path/TestProject.csproj");
private TextBufferProjectService UnsupportedProjectService { get; } = Mock.Of<TextBufferProjectService>(s => s.IsSupportedProject(It.IsAny<object>()) == false);
[Fact]
public void OnTextViewOpened_ForNonRazorCoreProject_DoesNothing()
{
// Arrange
var editorFactoryService = new Mock<RazorEditorFactoryService>(MockBehavior.Strict);
var documentManager = new DefaultRazorDocumentManager(editorFactoryService.Object, UnsupportedProjectService);
var textView = Mock.Of<ITextView>();
var buffers = new Collection<ITextBuffer>()
{
Mock.Of<ITextBuffer>(b => b.ContentType == RazorContentType && b.Properties == new PropertyCollection()),
};
// Act & Assert
documentManager.OnTextViewOpened(textView, buffers);
}
[Fact]
public void OnTextViewOpened_ForNonRazorTextBuffer_DoesNothing()
{
// Arrange
var editorFactoryService = new Mock<RazorEditorFactoryService>(MockBehavior.Strict);
var documentManager = new DefaultRazorDocumentManager(editorFactoryService.Object, SupportedProjectService);
var textView = Mock.Of<ITextView>();
var buffers = new Collection<ITextBuffer>()
{
Mock.Of<ITextBuffer>(b => b.ContentType == NonRazorContentType && b.Properties == new PropertyCollection()),
};
// Act & Assert
documentManager.OnTextViewOpened(textView, buffers);
}
[Fact]
public void OnTextViewOpened_ForRazorTextBuffer_AddsTextViewToTracker()
{
// Arrange
var textView = Mock.Of<ITextView>();
var buffers = new Collection<ITextBuffer>()
{
Mock.Of<ITextBuffer>(b => b.ContentType == RazorContentType && b.Properties == new PropertyCollection()),
};
var documentTracker = new DefaultVisualStudioDocumentTracker(FilePath, ProjectPath, ProjectManager, EditorSettingsManager, Workspace, buffers[0]) as VisualStudioDocumentTracker;
var editorFactoryService = Mock.Of<RazorEditorFactoryService>(factoryService => factoryService.TryGetDocumentTracker(It.IsAny<ITextBuffer>(), out documentTracker) == true);
var documentManager = new DefaultRazorDocumentManager(editorFactoryService, SupportedProjectService);
// Act
documentManager.OnTextViewOpened(textView, buffers);
// Assert
Assert.Collection(documentTracker.TextViews, v => Assert.Same(v, textView));
}
[Fact]
public void OnTextViewOpened_SubscribesAfterFirstTextViewOpened()
{
// Arrange
var textView = Mock.Of<ITextView>();
var buffers = new Collection<ITextBuffer>()
{
Mock.Of<ITextBuffer>(b => b.ContentType == RazorContentType && b.Properties == new PropertyCollection()),
Mock.Of<ITextBuffer>(b => b.ContentType == NonRazorContentType && b.Properties == new PropertyCollection()),
};
var documentTracker = new DefaultVisualStudioDocumentTracker(FilePath, ProjectPath, ProjectManager, EditorSettingsManager, Workspace, buffers[0]) as VisualStudioDocumentTracker;
var editorFactoryService = Mock.Of<RazorEditorFactoryService>(f => f.TryGetDocumentTracker(It.IsAny<ITextBuffer>(), out documentTracker) == true);
var documentManager = new DefaultRazorDocumentManager(editorFactoryService, SupportedProjectService);
// Assert 1
Assert.False(documentTracker.IsSupportedProject);
// Act
documentManager.OnTextViewOpened(textView, buffers);
// Assert 2
Assert.True(documentTracker.IsSupportedProject);
}
[Fact]
public void OnTextViewClosed_FoNonRazorCoreProject_DoesNothing()
{
// Arrange
var documentManager = new DefaultRazorDocumentManager(Mock.Of<RazorEditorFactoryService>(), UnsupportedProjectService);
var textView = Mock.Of<ITextView>();
var buffers = new Collection<ITextBuffer>()
{
Mock.Of<ITextBuffer>(b => b.ContentType == RazorContentType && b.Properties == new PropertyCollection()),
};
// Act
documentManager.OnTextViewClosed(textView, buffers);
// Assert
Assert.False(buffers[0].Properties.ContainsProperty(typeof(VisualStudioDocumentTracker)));
}
[Fact]
public void OnTextViewClosed_TextViewWithoutDocumentTracker_DoesNothing()
{
// Arrange
var documentManager = new DefaultRazorDocumentManager(Mock.Of<RazorEditorFactoryService>(), SupportedProjectService);
var textView = Mock.Of<ITextView>();
var buffers = new Collection<ITextBuffer>()
{
Mock.Of<ITextBuffer>(b => b.ContentType == RazorContentType && b.Properties == new PropertyCollection()),
};
// Act
documentManager.OnTextViewClosed(textView, buffers);
// Assert
Assert.False(buffers[0].Properties.ContainsProperty(typeof(VisualStudioDocumentTracker)));
}
[Fact]
public void OnTextViewClosed_ForAnyTextBufferWithTracker_RemovesTextView()
{
// Arrange
var textView1 = Mock.Of<ITextView>();
var textView2 = Mock.Of<ITextView>();
var buffers = new Collection<ITextBuffer>()
{
Mock.Of<ITextBuffer>(b => b.ContentType == RazorContentType && b.Properties == new PropertyCollection()),
Mock.Of<ITextBuffer>(b => b.ContentType == NonRazorContentType && b.Properties == new PropertyCollection()),
};
// 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]);
documentTracker.AddTextView(textView1);
documentTracker.AddTextView(textView2);
buffers[0].Properties.AddProperty(typeof(VisualStudioDocumentTracker), documentTracker);
documentTracker = new DefaultVisualStudioDocumentTracker(FilePath, ProjectPath, ProjectManager, EditorSettingsManager, Workspace, buffers[1]);
documentTracker.AddTextView(textView1);
documentTracker.AddTextView(textView2);
buffers[1].Properties.AddProperty(typeof(VisualStudioDocumentTracker), documentTracker);
var editorFactoryService = Mock.Of<RazorEditorFactoryService>();
var documentManager = new DefaultRazorDocumentManager(editorFactoryService, SupportedProjectService);
// Act
documentManager.OnTextViewClosed(textView2, buffers);
// Assert
documentTracker = buffers[0].Properties.GetProperty<DefaultVisualStudioDocumentTracker>(typeof(VisualStudioDocumentTracker));
Assert.Collection(documentTracker.TextViews, v => Assert.Same(v, textView1));
documentTracker = buffers[1].Properties.GetProperty<DefaultVisualStudioDocumentTracker>(typeof(VisualStudioDocumentTracker));
Assert.Collection(documentTracker.TextViews, v => Assert.Same(v, textView1));
}
[Fact]
public void OnTextViewClosed_UnsubscribesAfterLastTextViewClosed()
{
// Arrange
var textView1 = Mock.Of<ITextView>();
var textView2 = Mock.Of<ITextView>();
var buffers = new Collection<ITextBuffer>()
{
Mock.Of<ITextBuffer>(b => b.ContentType == RazorContentType && b.Properties == new PropertyCollection()),
Mock.Of<ITextBuffer>(b => b.ContentType == NonRazorContentType && b.Properties == new PropertyCollection()),
};
var documentTracker = new DefaultVisualStudioDocumentTracker(FilePath, ProjectPath, ProjectManager, EditorSettingsManager, Workspace, buffers[0]);
buffers[0].Properties.AddProperty(typeof(VisualStudioDocumentTracker), documentTracker);
var editorFactoryService = Mock.Of<RazorEditorFactoryService>();
var documentManager = new DefaultRazorDocumentManager(editorFactoryService, SupportedProjectService);
// Populate the text views
documentTracker.Subscribe();
documentTracker.AddTextView(textView1);
documentTracker.AddTextView(textView2);
// Act 1
documentManager.OnTextViewClosed(textView2, buffers);
// Assert 1
Assert.True(documentTracker.IsSupportedProject);
// Act
documentManager.OnTextViewClosed(textView1, buffers);
// Assert 2
Assert.False(documentTracker.IsSupportedProject);
}
}
}

View File

@ -6,14 +6,13 @@ using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Razor;
using Microsoft.CodeAnalysis.Razor.Editor;
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
using Microsoft.VisualStudio.Shell.Interop;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio.Utilities;
using Moq;
using Xunit;
namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor
namespace Microsoft.VisualStudio.Editor.Razor
{
public class DefaultVisualStudioDocumentTrackerTest
{
@ -23,12 +22,9 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor
private string FilePath => "C:/Some/Path/TestDocumentTracker.cshtml";
private ProjectSnapshotManager ProjectManager => Mock.Of<ProjectSnapshotManager>(p => p.Projects == new List<ProjectSnapshot>());
private string ProjectPath => "C:/Some/Path/TestProject.csproj";
private TextBufferProjectService ProjectService => Mock.Of<TextBufferProjectService>(
s => s.GetHierarchy(It.IsAny<ITextBuffer>()) == Mock.Of<IVsHierarchy>() &&
s.IsSupportedProject(It.IsAny<IVsHierarchy>()) == true &&
s.GetProjectPath(It.IsAny<IVsHierarchy>()) == "C:/Some/Path/TestProject.csproj");
private ProjectSnapshotManager ProjectManager => Mock.Of<ProjectSnapshotManager>(p => p.Projects == new List<ProjectSnapshot>());
private EditorSettingsManagerInternal EditorSettingsManager => new DefaultEditorSettingsManagerInternal();
@ -38,11 +34,12 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor
public void EditorSettingsManager_Changed_TriggersContextChanged()
{
// Arrange
var documentTracker = new DefaultVisualStudioDocumentTracker(FilePath, ProjectManager, ProjectService, EditorSettingsManager, Workspace, TextBuffer);
var documentTracker = new DefaultVisualStudioDocumentTracker(FilePath, ProjectPath, ProjectManager, EditorSettingsManager, Workspace, TextBuffer);
var called = false;
documentTracker.ContextChanged += (sender, args) =>
{
called = true;
Assert.Equal(ContextChangeKind.EditorSettingsChanged, args.Kind);
};
// Act
@ -52,11 +49,55 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor
Assert.True(called);
}
[Fact]
public void Subscribe_SetsSupportedProjectAndTriggersContextChanged()
{
// Arrange
var documentTracker = new DefaultVisualStudioDocumentTracker(FilePath, ProjectPath, ProjectManager, EditorSettingsManager, Workspace, TextBuffer);
var called = false;
documentTracker.ContextChanged += (sender, args) =>
{
called = true;
Assert.Equal(ContextChangeKind.ProjectChanged, args.Kind);
};
// Act
documentTracker.Subscribe();
// Assert
Assert.True(called);
Assert.True(documentTracker.IsSupportedProject);
}
[Fact]
public void Unsubscribe_ResetsSupportedProjectAndTriggersContextChanged()
{
// Arrange
var documentTracker = new DefaultVisualStudioDocumentTracker(FilePath, ProjectPath, ProjectManager, EditorSettingsManager, Workspace, TextBuffer);
// Subscribe once to set supported project
documentTracker.Subscribe();
var called = false;
documentTracker.ContextChanged += (sender, args) =>
{
called = true;
Assert.Equal(ContextChangeKind.ProjectChanged, args.Kind);
};
// Act
documentTracker.Unsubscribe();
// Assert
Assert.False(documentTracker.IsSupportedProject);
Assert.True(called);
}
[Fact]
public void AddTextView_AddsToTextViewCollection()
{
// Arrange
var documentTracker = new DefaultVisualStudioDocumentTracker(FilePath, ProjectManager, ProjectService, EditorSettingsManager, Workspace, TextBuffer);
var documentTracker = new DefaultVisualStudioDocumentTracker(FilePath, ProjectPath, ProjectManager, EditorSettingsManager, Workspace, TextBuffer);
var textView = Mock.Of<ITextView>();
// Act
@ -66,28 +107,11 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor
Assert.Collection(documentTracker.TextViews, v => Assert.Same(v, textView));
}
[Fact]
public void AddTextView_SubscribesAfterFirstTextViewAdded()
{
// Arrange
var documentTracker = new DefaultVisualStudioDocumentTracker(FilePath, ProjectManager, ProjectService, EditorSettingsManager, Workspace, TextBuffer);
var textView = Mock.Of<ITextView>();
// Assert - 1
Assert.False(documentTracker.IsSupportedProject);
// Act
documentTracker.AddTextView(textView);
// Assert - 2
Assert.True(documentTracker.IsSupportedProject);
}
[Fact]
public void AddTextView_DoesNotAddDuplicateTextViews()
{
// Arrange
var documentTracker = new DefaultVisualStudioDocumentTracker(FilePath, ProjectManager, ProjectService, EditorSettingsManager, Workspace, TextBuffer);
var documentTracker = new DefaultVisualStudioDocumentTracker(FilePath, ProjectPath, ProjectManager, EditorSettingsManager, Workspace, TextBuffer);
var textView = Mock.Of<ITextView>();
// Act
@ -102,7 +126,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor
public void AddTextView_AddsMultipleTextViewsToCollection()
{
// Arrange
var documentTracker = new DefaultVisualStudioDocumentTracker(FilePath, ProjectManager, ProjectService, EditorSettingsManager, Workspace, TextBuffer);
var documentTracker = new DefaultVisualStudioDocumentTracker(FilePath, ProjectPath, ProjectManager, EditorSettingsManager, Workspace, TextBuffer);
var textView1 = Mock.Of<ITextView>();
var textView2 = Mock.Of<ITextView>();
@ -121,7 +145,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor
public void RemoveTextView_RemovesTextViewFromCollection_SingleItem()
{
// Arrange
var documentTracker = new DefaultVisualStudioDocumentTracker(FilePath, ProjectManager, ProjectService, EditorSettingsManager, Workspace, TextBuffer);
var documentTracker = new DefaultVisualStudioDocumentTracker(FilePath, ProjectPath, ProjectManager, EditorSettingsManager, Workspace, TextBuffer);
var textView = Mock.Of<ITextView>();
documentTracker.AddTextView(textView);
@ -136,7 +160,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor
public void RemoveTextView_RemovesTextViewFromCollection_MultipleItems()
{
// Arrange
var documentTracker = new DefaultVisualStudioDocumentTracker(FilePath, ProjectManager, ProjectService, EditorSettingsManager, Workspace, TextBuffer);
var documentTracker = new DefaultVisualStudioDocumentTracker(FilePath, ProjectPath, ProjectManager, EditorSettingsManager, Workspace, TextBuffer);
var textView1 = Mock.Of<ITextView>();
var textView2 = Mock.Of<ITextView>();
var textView3 = Mock.Of<ITextView>();
@ -158,7 +182,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor
public void RemoveTextView_NoopsWhenRemovingTextViewNotInCollection()
{
// Arrange
var documentTracker = new DefaultVisualStudioDocumentTracker(FilePath, ProjectManager, ProjectService, EditorSettingsManager, Workspace, TextBuffer);
var documentTracker = new DefaultVisualStudioDocumentTracker(FilePath, ProjectPath, ProjectManager, EditorSettingsManager, Workspace, TextBuffer);
var textView1 = Mock.Of<ITextView>();
documentTracker.AddTextView(textView1);
var textView2 = Mock.Of<ITextView>();
@ -169,28 +193,5 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor
// Assert
Assert.Collection(documentTracker.TextViews, v => Assert.Same(v, textView1));
}
[Fact]
public void RemoveTextView_UnsubscribesAfterLastTextViewRemoved()
{
// Arrange
var documentTracker = new DefaultVisualStudioDocumentTracker(FilePath, ProjectManager, ProjectService, EditorSettingsManager, Workspace, TextBuffer);
var textView1 = Mock.Of<ITextView>();
var textView2 = Mock.Of<ITextView>();
documentTracker.AddTextView(textView1);
documentTracker.AddTextView(textView2);
// Act - 1
documentTracker.RemoveTextView(textView1);
// Assert - 1
Assert.True(documentTracker.IsSupportedProject);
// Act - 2
documentTracker.RemoveTextView(textView2);
// Assert - 2
Assert.False(documentTracker.IsSupportedProject);
}
}
}

View File

@ -1,17 +1,11 @@
// 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.Collections.ObjectModel;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Razor;
using Microsoft.CodeAnalysis.Razor.Editor;
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
using Microsoft.VisualStudio.Editor.Razor;
using Microsoft.VisualStudio.Shell.Interop;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio.Utilities;
using Moq;
using Xunit;
@ -19,149 +13,42 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor
{
public class RazorTextViewConnectionListenerTest : ForegroundDispatcherTestBase
{
private ProjectSnapshotManager ProjectManager { get; } = Mock.Of<ProjectSnapshotManager>(p => p.Projects == new List<ProjectSnapshot>());
private TextBufferProjectService ProjectService { get; } = Mock.Of<TextBufferProjectService>(
s => s.GetHierarchy(It.IsAny<ITextBuffer>()) == Mock.Of<IVsHierarchy>() &&
s.IsSupportedProject(It.IsAny<IVsHierarchy>()) == true &&
s.GetProjectPath(It.IsAny<IVsHierarchy>()) == "C:/Some/Path/TestProject.csproj");
private EditorSettingsManagerInternal EditorSettingsManager => new DefaultEditorSettingsManagerInternal();
private Workspace Workspace { get; } = new AdhocWorkspace();
private IContentType RazorContentType { get; } = Mock.Of<IContentType>(c => c.IsOfType(RazorLanguage.ContentType) == true);
private IContentType NonRazorContentType { get; } = Mock.Of<IContentType>(c => c.IsOfType(It.IsAny<string>()) == false);
private TextBufferProjectService SupportedProjectService { get; } = Mock.Of<TextBufferProjectService>(s => s.IsSupportedProject(It.IsAny<IVsHierarchy>()) == true);
private TextBufferProjectService UnsupportedProjectService { get; } = Mock.Of<TextBufferProjectService>(s => s.IsSupportedProject(It.IsAny<IVsHierarchy>()) == false);
[ForegroundFact]
public void SubjectBuffersConnected_ForNonRazorCoreProject_DoesNothing()
{
// Arrange
var editorFactoryService = new Mock<RazorEditorFactoryService>(MockBehavior.Strict);
var factory = new RazorTextViewConnectionListener(Dispatcher, UnsupportedProjectService, editorFactoryService.Object, Workspace);
var textView = Mock.Of<IWpfTextView>();
var buffers = new Collection<ITextBuffer>()
{
Mock.Of<ITextBuffer>(b => b.ContentType == RazorContentType && b.Properties == new PropertyCollection()),
};
// Act & Assert
factory.SubjectBuffersConnected(textView, ConnectionReason.BufferGraphChange, buffers);
}
[ForegroundFact]
public void SubjectBuffersConnected_ForNonRazorTextBuffer_DoesNothing()
{
// Arrange
var editorFactoryService = new Mock<RazorEditorFactoryService>(MockBehavior.Strict);
var factory = new RazorTextViewConnectionListener(Dispatcher, SupportedProjectService, editorFactoryService.Object, Workspace);
var textView = Mock.Of<IWpfTextView>();
var buffers = new Collection<ITextBuffer>()
{
Mock.Of<ITextBuffer>(b => b.ContentType == NonRazorContentType && b.Properties == new PropertyCollection()),
};
// Act & Assert
factory.SubjectBuffersConnected(textView, ConnectionReason.BufferGraphChange, buffers);
}
[ForegroundFact]
public void SubjectBuffersConnected_ForRazorTextBuffer_AddsTextViewToTracker()
public void SubjectBuffersConnected_CallsRazorDocumentManager_OnTextViewOpened()
{
// Arrange
var textView = Mock.Of<IWpfTextView>();
var buffers = new Collection<ITextBuffer>()
{
Mock.Of<ITextBuffer>(b => b.ContentType == RazorContentType && b.Properties == new PropertyCollection()),
};
VisualStudioDocumentTracker documentTracker = new DefaultVisualStudioDocumentTracker("AFile", ProjectManager, ProjectService, EditorSettingsManager, Workspace, buffers[0]);
var editorFactoryService = Mock.Of<RazorEditorFactoryService>(factoryService => factoryService.TryGetDocumentTracker(It.IsAny<ITextBuffer>(), out documentTracker) == true);
var textViewListener = new RazorTextViewConnectionListener(Dispatcher, SupportedProjectService, editorFactoryService, Workspace);
var buffers = new Collection<ITextBuffer>();
var workspace = new AdhocWorkspace();
var documentManager = new Mock<RazorDocumentManager>(MockBehavior.Strict);
documentManager.Setup(d => d.OnTextViewOpened(textView, buffers)).Verifiable();
var listener = new RazorTextViewConnectionListener(Dispatcher, workspace, documentManager.Object);
// Act
textViewListener.SubjectBuffersConnected(textView, ConnectionReason.BufferGraphChange, buffers);
listener.SubjectBuffersConnected(textView, ConnectionReason.BufferGraphChange, buffers);
// Assert
Assert.Collection(documentTracker.TextViews, v => Assert.Same(v, textView));
documentManager.Verify();
}
[ForegroundFact]
public void SubjectBuffersDisconnected_ForAnyTextBufferWithTracker_RemovesTextView()
public void SubjectBuffersDisonnected_CallsRazorDocumentManager_OnTextViewClosed()
{
// Arrange
var textView1 = Mock.Of<IWpfTextView>();
var textView2 = Mock.Of<IWpfTextView>();
var buffers = new Collection<ITextBuffer>()
{
Mock.Of<ITextBuffer>(b => b.ContentType == RazorContentType && b.Properties == new PropertyCollection()),
Mock.Of<ITextBuffer>(b => b.ContentType == NonRazorContentType && b.Properties == new PropertyCollection()),
};
// Preload the buffer's properties with a tracker, so it's like we've already tracked this one.
var tracker = new DefaultVisualStudioDocumentTracker("C:/File/Path/To/Tracker1.cshtml", ProjectManager, ProjectService, EditorSettingsManager, Workspace, buffers[0]);
tracker.AddTextView(textView1);
tracker.AddTextView(textView2);
buffers[0].Properties.AddProperty(typeof(VisualStudioDocumentTracker), tracker);
tracker = new DefaultVisualStudioDocumentTracker("C:/File/Path/To/Tracker1.cshtml", ProjectManager, ProjectService, EditorSettingsManager, Workspace, buffers[1]);
tracker.AddTextView(textView1);
tracker.AddTextView(textView2);
buffers[1].Properties.AddProperty(typeof(VisualStudioDocumentTracker), tracker);
var textViewListener = new RazorTextViewConnectionListener(Dispatcher, SupportedProjectService, Mock.Of<RazorEditorFactoryService>(), Workspace);
// Act
textViewListener.SubjectBuffersDisconnected(textView2, ConnectionReason.BufferGraphChange, buffers);
// Assert
tracker = buffers[0].Properties.GetProperty<DefaultVisualStudioDocumentTracker>(typeof(VisualStudioDocumentTracker));
Assert.Collection(tracker.TextViews, v => Assert.Same(v, textView1));
tracker = buffers[1].Properties.GetProperty<DefaultVisualStudioDocumentTracker>(typeof(VisualStudioDocumentTracker));
Assert.Collection(tracker.TextViews, v => Assert.Same(v, textView1));
}
[ForegroundFact]
public void SubjectBuffersDisconnected_FoNonRazorCoreProject_DoesNothing()
{
// Arrange
var textViewListener = new RazorTextViewConnectionListener(Dispatcher, UnsupportedProjectService, Mock.Of<RazorEditorFactoryService>(), Workspace);
var textView = Mock.Of<IWpfTextView>();
var buffers = new Collection<ITextBuffer>()
{
Mock.Of<ITextBuffer>(b => b.ContentType == RazorContentType && b.Properties == new PropertyCollection()),
};
var buffers = new Collection<ITextBuffer>();
var workspace = new AdhocWorkspace();
var documentManager = new Mock<RazorDocumentManager>(MockBehavior.Strict);
documentManager.Setup(d => d.OnTextViewClosed(textView, buffers)).Verifiable();
var listener = new RazorTextViewConnectionListener(Dispatcher, workspace, documentManager.Object);
// Act
textViewListener.SubjectBuffersDisconnected(textView, ConnectionReason.BufferGraphChange, buffers);
listener.SubjectBuffersDisconnected(textView, ConnectionReason.BufferGraphChange, buffers);
// Assert
Assert.False(buffers[0].Properties.ContainsProperty(typeof(VisualStudioDocumentTracker)));
}
[ForegroundFact]
public void SubjectBuffersDisconnected_ForAnyTextBufferWithoutTracker_DoesNothing()
{
// Arrange
var textViewListener = new RazorTextViewConnectionListener(Dispatcher, SupportedProjectService, Mock.Of<RazorEditorFactoryService>(), Workspace);
var textView = Mock.Of<IWpfTextView>();
var buffers = new Collection<ITextBuffer>()
{
Mock.Of<ITextBuffer>(b => b.ContentType == RazorContentType && b.Properties == new PropertyCollection()),
};
// Act
textViewListener.SubjectBuffersDisconnected(textView, ConnectionReason.BufferGraphChange, buffers);
// Assert
Assert.False(buffers[0].Properties.ContainsProperty(typeof(VisualStudioDocumentTracker)));
documentManager.Verify();
}
}
}