From 6d2460ae7fe6631b956750c31d080950ab77d6dd Mon Sep 17 00:00:00 2001 From: "N. Taylor Mullen" Date: Tue, 30 Jan 2018 12:30:54 -0800 Subject: [PATCH] Update DefaultRazorEditorFactoryService to be per-workspace. - Updated the a `VisualStudioWorkspaceAccessor` API in windows to enable the factory to retrieve a workspace given a text buffer. - Added a way to add test services to `AdhocWorkspace` so we can test against services being retrieved from a `Workspace`. This will be much more common once we rely on services coming from `TextBuffer`s in our other tooling pieces. - Added tests for the default workspace provider. #1989 --- .../DefaultRazorEditorFactoryService.cs | 80 +++++----- .../VisualStudioWorkspaceAccessor.cs | 3 + .../DefaultVisualStudioWorkspaceAccessor.cs | 104 +++++++++++- .../DefaultVisualStudioWorkspaceAccessor.cs | 6 + ...alysis.Razor.Workspaces.Test.Common.csproj | 1 + .../TestRazorLanguageServices.cs | 48 ++++++ .../TestServices.cs | 47 ++++++ .../TestWorkspace.cs | 7 +- .../TestWorkspaceServices.cs | 85 ++++++++++ .../DefaultRazorEditorFactoryServiceTest.cs | 71 +++++++-- ...efaultVisualStudioWorkspaceAccessorTest.cs | 150 ++++++++++++++++++ 11 files changed, 539 insertions(+), 63 deletions(-) create mode 100644 test/Microsoft.CodeAnalysis.Razor.Workspaces.Test.Common/TestRazorLanguageServices.cs create mode 100644 test/Microsoft.CodeAnalysis.Razor.Workspaces.Test.Common/TestServices.cs create mode 100644 test/Microsoft.CodeAnalysis.Razor.Workspaces.Test.Common/TestWorkspaceServices.cs create mode 100644 test/Microsoft.VisualStudio.LanguageServices.Razor.Test/DefaultVisualStudioWorkspaceAccessorTest.cs diff --git a/src/Microsoft.VisualStudio.Editor.Razor/DefaultRazorEditorFactoryService.cs b/src/Microsoft.VisualStudio.Editor.Razor/DefaultRazorEditorFactoryService.cs index c795c1462d..66f624d02d 100644 --- a/src/Microsoft.VisualStudio.Editor.Razor/DefaultRazorEditorFactoryService.cs +++ b/src/Microsoft.VisualStudio.Editor.Razor/DefaultRazorEditorFactoryService.cs @@ -14,10 +14,7 @@ namespace Microsoft.VisualStudio.Editor.Razor internal class DefaultRazorEditorFactoryService : RazorEditorFactoryService { private static readonly object RazorTextBufferInitializationKey = new object(); - - private readonly VisualStudioDocumentTrackerFactory _documentTrackerFactory; - private readonly VisualStudioRazorParserFactory _parserFactory; - private readonly BraceSmartIndenterFactory _braceSmartIndenterFactory; + private readonly VisualStudioWorkspaceAccessor _workspaceAccessor; [ImportingConstructor] public DefaultRazorEditorFactoryService(VisualStudioWorkspaceAccessor workspaceAccessor) @@ -27,36 +24,7 @@ namespace Microsoft.VisualStudio.Editor.Razor throw new ArgumentNullException(nameof(workspaceAccessor)); } - var razorLanguageServices = workspaceAccessor.Workspace.Services.GetLanguageServices(RazorLanguage.Name); - _documentTrackerFactory = razorLanguageServices.GetRequiredService(); - _parserFactory = razorLanguageServices.GetRequiredService(); - _braceSmartIndenterFactory = razorLanguageServices.GetRequiredService(); - } - - // Internal for testing - internal DefaultRazorEditorFactoryService( - VisualStudioDocumentTrackerFactory documentTrackerFactory, - VisualStudioRazorParserFactory parserFactory, - BraceSmartIndenterFactory braceSmartIndenterFactory) - { - if (documentTrackerFactory == null) - { - throw new ArgumentNullException(nameof(documentTrackerFactory)); - } - - if (parserFactory == null) - { - throw new ArgumentNullException(nameof(parserFactory)); - } - - if (braceSmartIndenterFactory == null) - { - throw new ArgumentNullException(nameof(braceSmartIndenterFactory)); - } - - _documentTrackerFactory = documentTrackerFactory; - _parserFactory = parserFactory; - _braceSmartIndenterFactory = braceSmartIndenterFactory; + _workspaceAccessor = workspaceAccessor; } public override bool TryGetDocumentTracker(ITextBuffer textBuffer, out VisualStudioDocumentTracker documentTracker) @@ -72,7 +40,12 @@ namespace Microsoft.VisualStudio.Editor.Razor return false; } - EnsureTextBufferInitialized(textBuffer); + var textBufferInitialized = TryInitializeTextBuffer(textBuffer); + if (!textBufferInitialized) + { + documentTracker = null; + return false; + } if (!textBuffer.Properties.TryGetProperty(typeof(VisualStudioDocumentTracker), out documentTracker)) { @@ -96,7 +69,12 @@ namespace Microsoft.VisualStudio.Editor.Razor return false; } - EnsureTextBufferInitialized(textBuffer); + var textBufferInitialized = TryInitializeTextBuffer(textBuffer); + if (!textBufferInitialized) + { + parser = null; + return false; + } if (!textBuffer.Properties.TryGetProperty(typeof(VisualStudioRazorParser), out parser)) { @@ -120,7 +98,12 @@ namespace Microsoft.VisualStudio.Editor.Razor return false; } - EnsureTextBufferInitialized(textBuffer); + var textBufferInitialized = TryInitializeTextBuffer(textBuffer); + if (!textBufferInitialized) + { + braceSmartIndenter = null; + return false; + } if (!textBuffer.Properties.TryGetProperty(typeof(BraceSmartIndenter), out braceSmartIndenter)) { @@ -132,24 +115,37 @@ namespace Microsoft.VisualStudio.Editor.Razor } // Internal for testing - internal void EnsureTextBufferInitialized(ITextBuffer textBuffer) + internal bool TryInitializeTextBuffer(ITextBuffer textBuffer) { if (textBuffer.Properties.ContainsProperty(RazorTextBufferInitializationKey)) { // Buffer already initialized. - return; + return true; } - var tracker = _documentTrackerFactory.Create(textBuffer); + if (!_workspaceAccessor.TryGetWorkspace(textBuffer, out var workspace)) + { + // Could not locate workspace for given text buffer. + return false; + } + + var razorLanguageServices = workspace.Services.GetLanguageServices(RazorLanguage.Name); + var documentTrackerFactory = razorLanguageServices.GetRequiredService(); + var parserFactory = razorLanguageServices.GetRequiredService(); + var braceSmartIndenterFactory = razorLanguageServices.GetRequiredService(); + + var tracker = documentTrackerFactory.Create(textBuffer); textBuffer.Properties[typeof(VisualStudioDocumentTracker)] = tracker; - var parser = _parserFactory.Create(tracker); + var parser = parserFactory.Create(tracker); textBuffer.Properties[typeof(VisualStudioRazorParser)] = parser; - var braceSmartIndenter = _braceSmartIndenterFactory.Create(tracker); + var braceSmartIndenter = braceSmartIndenterFactory.Create(tracker); textBuffer.Properties[typeof(BraceSmartIndenter)] = braceSmartIndenter; textBuffer.Properties.AddProperty(RazorTextBufferInitializationKey, RazorTextBufferInitializationKey); + + return true; } } } diff --git a/src/Microsoft.VisualStudio.Editor.Razor/VisualStudioWorkspaceAccessor.cs b/src/Microsoft.VisualStudio.Editor.Razor/VisualStudioWorkspaceAccessor.cs index 2e84e16ec8..3cd69cb1a5 100644 --- a/src/Microsoft.VisualStudio.Editor.Razor/VisualStudioWorkspaceAccessor.cs +++ b/src/Microsoft.VisualStudio.Editor.Razor/VisualStudioWorkspaceAccessor.cs @@ -2,11 +2,14 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using Microsoft.CodeAnalysis; +using Microsoft.VisualStudio.Text; namespace Microsoft.VisualStudio.Editor.Razor { internal abstract class VisualStudioWorkspaceAccessor { public abstract Workspace Workspace { get; } + + public abstract bool TryGetWorkspace(ITextBuffer textBuffer, out Workspace workspace); } } diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/DefaultVisualStudioWorkspaceAccessor.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/DefaultVisualStudioWorkspaceAccessor.cs index 4749c44f5b..69209007db 100644 --- a/src/Microsoft.VisualStudio.LanguageServices.Razor/DefaultVisualStudioWorkspaceAccessor.cs +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/DefaultVisualStudioWorkspaceAccessor.cs @@ -1,9 +1,14 @@ // 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.ComponentModel.Composition; +using System.Linq; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Text; using Microsoft.VisualStudio.Editor.Razor; +using Microsoft.VisualStudio.Text; +using Microsoft.VisualStudio.Text.Projection; namespace Microsoft.VisualStudio.LanguageServices.Razor { @@ -11,12 +16,105 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor [Export(typeof(VisualStudioWorkspaceAccessor))] internal class DefaultVisualStudioWorkspaceAccessor : VisualStudioWorkspaceAccessor { + private readonly IBufferGraphFactoryService _bufferGraphService; + private readonly TextBufferProjectService _projectService; + private readonly Workspace _defaultWorkspace; + [ImportingConstructor] - public DefaultVisualStudioWorkspaceAccessor([Import(typeof(VisualStudioWorkspace))] Workspace workspace) + public DefaultVisualStudioWorkspaceAccessor( + IBufferGraphFactoryService bufferGraphService, + TextBufferProjectService projectService, + [Import(typeof(VisualStudioWorkspace))] Workspace defaultWorkspace) { - Workspace = workspace; + if (bufferGraphService == null) + { + throw new ArgumentNullException(nameof(bufferGraphService)); + } + + if (projectService == null) + { + throw new ArgumentNullException(nameof(projectService)); + } + + if (defaultWorkspace == null) + { + throw new ArgumentNullException(nameof(defaultWorkspace)); + } + + _bufferGraphService = bufferGraphService; + _projectService = projectService; + _defaultWorkspace = defaultWorkspace; } - public override Workspace Workspace { get; } + public override Workspace Workspace => _defaultWorkspace; + + public override bool TryGetWorkspace(ITextBuffer textBuffer, out Workspace workspace) + { + if (textBuffer == null) + { + throw new ArgumentNullException(nameof(textBuffer)); + } + + // We do a best effort approach in this method to get the workspace that belongs to the TextBuffer. + // The approaches we take to find the workspace are: + // + // 1. Look for a C# projection buffer associated with the Razor buffer. If we can find one we let + // Roslyn take control of finding the Workspace (projectionBuffer.GetWorkspace()). If not, + // fall back to determining if we can use the default workspace. + // 2. Look to see if this ITextBuffer is associated with a host project. If we find that our Razor + // buffer has a host project, we make the assumption that we should use the default VisualStudioWorkspace. + + if (TryGetWorkspaceFromProjectionBuffer(textBuffer, out workspace)) + { + return true; + } + + if (TryGetWorkspaceFromHostProject(textBuffer, out workspace)) + { + return true; + } + + workspace = null; + return false; + } + + // Internal virtual for testing + internal virtual bool TryGetWorkspaceFromProjectionBuffer(ITextBuffer textBuffer, out Workspace workspace) + { + var graph = _bufferGraphService.CreateBufferGraph(textBuffer); + var projectedCSharpBuffer = graph.GetTextBuffers(buffer => buffer.ContentType.IsOfType("CSharp")).FirstOrDefault(); + + if (projectedCSharpBuffer == null) + { + workspace = null; + return false; + } + + workspace = projectedCSharpBuffer.GetWorkspace(); + if (workspace == null) + { + // Couldn't resolve a workspace for the projected csharp buffer. + return false; + } + + return true; + } + + // Internal virtual for testing + internal virtual bool TryGetWorkspaceFromHostProject(ITextBuffer textBuffer, out Workspace workspace) + { + var project = _projectService.GetHostProject(textBuffer); + + if (project == null) + { + // Could not locate a project for the given text buffer. + workspace = null; + return false; + } + + // We have a host project, assume default workspace. + workspace = _defaultWorkspace; + return true; + } } } diff --git a/src/Microsoft.VisualStudio.Mac.LanguageServices.Razor/DefaultVisualStudioWorkspaceAccessor.cs b/src/Microsoft.VisualStudio.Mac.LanguageServices.Razor/DefaultVisualStudioWorkspaceAccessor.cs index 0b3ab60bcc..0fb24bc1bd 100644 --- a/src/Microsoft.VisualStudio.Mac.LanguageServices.Razor/DefaultVisualStudioWorkspaceAccessor.cs +++ b/src/Microsoft.VisualStudio.Mac.LanguageServices.Razor/DefaultVisualStudioWorkspaceAccessor.cs @@ -4,6 +4,7 @@ using System.ComponentModel.Composition; using Microsoft.CodeAnalysis; using Microsoft.VisualStudio.Editor.Razor; +using Microsoft.VisualStudio.Text; using MonoDevelop.Ide.TypeSystem; namespace Microsoft.VisualStudio.Mac.LanguageServices.Razor @@ -18,5 +19,10 @@ namespace Microsoft.VisualStudio.Mac.LanguageServices.Razor } public override Workspace Workspace { get; } + + public override bool TryGetWorkspace(ITextBuffer textBuffer, out Workspace workspace) + { + throw new System.NotImplementedException(); + } } } \ No newline at end of file diff --git a/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test.Common/Microsoft.CodeAnalysis.Razor.Workspaces.Test.Common.csproj b/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test.Common/Microsoft.CodeAnalysis.Razor.Workspaces.Test.Common.csproj index 9952960ebb..c32bd85577 100644 --- a/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test.Common/Microsoft.CodeAnalysis.Razor.Workspaces.Test.Common.csproj +++ b/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test.Common/Microsoft.CodeAnalysis.Razor.Workspaces.Test.Common.csproj @@ -6,6 +6,7 @@ + diff --git a/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test.Common/TestRazorLanguageServices.cs b/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test.Common/TestRazorLanguageServices.cs new file mode 100644 index 0000000000..fe33148ab0 --- /dev/null +++ b/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test.Common/TestRazorLanguageServices.cs @@ -0,0 +1,48 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.CodeAnalysis.Razor; + +namespace Microsoft.CodeAnalysis.Host +{ + internal class TestRazorLanguageServices : HostLanguageServices + { + private readonly HostWorkspaceServices _workspaceServices; + private readonly IEnumerable _languageServices; + + public TestRazorLanguageServices(HostWorkspaceServices workspaceServices, IEnumerable languageServices) + { + if (workspaceServices == null) + { + throw new ArgumentNullException(nameof(workspaceServices)); + } + + if (languageServices == null) + { + throw new ArgumentNullException(nameof(languageServices)); + } + + _workspaceServices = workspaceServices; + _languageServices = languageServices; + } + + public override HostWorkspaceServices WorkspaceServices => _workspaceServices; + + public override string Language => RazorLanguage.Name; + + public override TLanguageService GetService() + { + var service = _languageServices.OfType().FirstOrDefault(); + + if (service == null) + { + throw new InvalidOperationException($"Test Razor language services not configured properly, missing language service '{typeof(TLanguageService).FullName}'."); + } + + return service; + } + } +} diff --git a/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test.Common/TestServices.cs b/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test.Common/TestServices.cs new file mode 100644 index 0000000000..a9e53de621 --- /dev/null +++ b/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test.Common/TestServices.cs @@ -0,0 +1,47 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Microsoft.CodeAnalysis.Host +{ + public class TestServices : HostServices + { + private readonly IEnumerable _workspaceServices; + private readonly IEnumerable _razorLanguageServices; + + private TestServices(IEnumerable workspaceServices, IEnumerable razorLanguageServices) + { + if (workspaceServices == null) + { + throw new ArgumentNullException(nameof(workspaceServices)); + } + + if (razorLanguageServices == null) + { + throw new ArgumentNullException(nameof(razorLanguageServices)); + } + + _workspaceServices = workspaceServices; + _razorLanguageServices = razorLanguageServices; + } + + protected override HostWorkspaceServices CreateWorkspaceServices(Workspace workspace) + { + if (workspace == null) + { + throw new ArgumentNullException(nameof(workspace)); + } + + return new TestWorkspaceServices(this, _workspaceServices, _razorLanguageServices, workspace); + } + + public static HostServices Create(IEnumerable razorLanguageServices) + => Create(Enumerable.Empty(), razorLanguageServices); + + public static HostServices Create(IEnumerable workspaceServices, IEnumerable razorLanguageServices) + => new TestServices(workspaceServices, razorLanguageServices); + } +} diff --git a/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test.Common/TestWorkspace.cs b/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test.Common/TestWorkspace.cs index b375509708..02dcab53d5 100644 --- a/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test.Common/TestWorkspace.cs +++ b/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test.Common/TestWorkspace.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using Microsoft.CodeAnalysis.Host; namespace Microsoft.CodeAnalysis { @@ -9,11 +10,13 @@ namespace Microsoft.CodeAnalysis { private static readonly object WorkspaceLock = new object(); - public static Workspace Create(Action configure = null) + public static Workspace Create(Action configure = null) => Create(services: null, configure: configure); + + public static Workspace Create(HostServices services, Action configure = null) { lock (WorkspaceLock) { - var workspace = new AdhocWorkspace(); + var workspace = services == null ? new AdhocWorkspace() : new AdhocWorkspace(services); configure?.Invoke(workspace); return workspace; diff --git a/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test.Common/TestWorkspaceServices.cs b/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test.Common/TestWorkspaceServices.cs new file mode 100644 index 0000000000..66d8f42f27 --- /dev/null +++ b/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test.Common/TestWorkspaceServices.cs @@ -0,0 +1,85 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.CodeAnalysis.Razor; + +namespace Microsoft.CodeAnalysis.Host +{ + internal class TestWorkspaceServices : HostWorkspaceServices + { + private static readonly Workspace DefaultWorkspace = TestWorkspace.Create(); + + private readonly HostServices _hostServices; + private readonly IEnumerable _workspaceServices; + private readonly TestRazorLanguageServices _razorLanguageServices; + private readonly Workspace _workspace; + + public TestWorkspaceServices( + HostServices hostServices, + IEnumerable workspaceServices, + IEnumerable razorLanguageServices, + Workspace workspace) + { + if (hostServices == null) + { + throw new ArgumentNullException(nameof(hostServices)); + } + + if (workspaceServices == null) + { + throw new ArgumentNullException(nameof(workspaceServices)); + } + + if (razorLanguageServices == null) + { + throw new ArgumentNullException(nameof(razorLanguageServices)); + } + + if (workspace == null) + { + throw new ArgumentNullException(nameof(workspace)); + } + + _hostServices = hostServices; + _workspaceServices = workspaceServices; + _razorLanguageServices = new TestRazorLanguageServices(this, razorLanguageServices); + _workspace = workspace; + } + + public override HostServices HostServices => _hostServices; + + public override Workspace Workspace => _workspace; + + public override TWorkspaceService GetService() + { + var service = _workspaceServices.OfType().FirstOrDefault(); + + if (service == null) + { + // Fallback to default host services to resolve roslyn specific features. + service = DefaultWorkspace.Services.GetService(); + } + + return service; + } + + public override HostLanguageServices GetLanguageServices(string languageName) + { + if (languageName != RazorLanguage.Name) + { + throw new InvalidOperationException($"Test services do not support language service '{languageName}'. The only language services supported are '{RazorLanguage.Name}'."); + } + + return _razorLanguageServices; + } + + public override IEnumerable SupportedLanguages => new[] { RazorLanguage.Name }; + + public override bool IsSupported(string languageName) => languageName == RazorLanguage.Name; + + public override IEnumerable FindLanguageServices(MetadataFilter filter) => throw new NotImplementedException(); + } +} diff --git a/test/Microsoft.VisualStudio.Editor.Razor.Test/DefaultRazorEditorFactoryServiceTest.cs b/test/Microsoft.VisualStudio.Editor.Razor.Test/DefaultRazorEditorFactoryServiceTest.cs index a590901752..49b95010c2 100644 --- a/test/Microsoft.VisualStudio.Editor.Razor.Test/DefaultRazorEditorFactoryServiceTest.cs +++ b/test/Microsoft.VisualStudio.Editor.Razor.Test/DefaultRazorEditorFactoryServiceTest.cs @@ -1,6 +1,8 @@ // 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.CodeAnalysis; +using Microsoft.CodeAnalysis.Host; using Microsoft.CodeAnalysis.Razor; using Microsoft.VisualStudio.Text; using Microsoft.VisualStudio.Utilities; @@ -47,7 +49,25 @@ namespace Microsoft.VisualStudio.Editor.Razor } [Fact] - public void EnsureTextBufferInitialized_StoresTracker() + public void TryInitializeTextBuffer_WorkspaceAccessorCanNotAccessWorkspace_ReturnsFalse() + { + // Arrange + Workspace workspace = null; + var workspaceAccessor = new Mock(); + workspaceAccessor.Setup(provider => provider.TryGetWorkspace(It.IsAny(), out workspace)) + .Returns(false); + var factoryService = new DefaultRazorEditorFactoryService(workspaceAccessor.Object); + var textBuffer = Mock.Of(b => b.ContentType == RazorCoreContentType && b.Properties == new PropertyCollection()); + + // Act + var result = factoryService.TryInitializeTextBuffer(textBuffer); + + // Assert + Assert.False(result); + } + + [Fact] + public void TryInitializeTextBuffer_StoresTracker_ReturnsTrue() { // Arrange var expectedDocumentTracker = Mock.Of(); @@ -55,29 +75,31 @@ namespace Microsoft.VisualStudio.Editor.Razor var textBuffer = Mock.Of(b => b.ContentType == RazorCoreContentType && b.Properties == new PropertyCollection()); // Act - factoryService.EnsureTextBufferInitialized(textBuffer); + var result = factoryService.TryInitializeTextBuffer(textBuffer); // Assert + Assert.True(result); Assert.True(textBuffer.Properties.TryGetProperty(typeof(VisualStudioDocumentTracker), out VisualStudioDocumentTracker documentTracker)); Assert.Same(expectedDocumentTracker, documentTracker); } [Fact] - public void EnsureTextBufferInitialized_OnlyStoresTrackerOnTextBufferOnce() + public void TryInitializeTextBuffer_OnlyStoresTrackerOnTextBufferOnce_ReturnsTrue() { // Arrange var factoryService = CreateFactoryService(); var textBuffer = Mock.Of(b => b.ContentType == RazorCoreContentType && b.Properties == new PropertyCollection()); - factoryService.EnsureTextBufferInitialized(textBuffer); + factoryService.TryInitializeTextBuffer(textBuffer); var expectedDocumentTracker = textBuffer.Properties[typeof(VisualStudioDocumentTracker)]; // Create a second factory service so it generates a different tracker factoryService = CreateFactoryService(); // Act - factoryService.EnsureTextBufferInitialized(textBuffer); + var result = factoryService.TryInitializeTextBuffer(textBuffer); // Assert + Assert.True(result); Assert.True(textBuffer.Properties.TryGetProperty(typeof(VisualStudioDocumentTracker), out VisualStudioDocumentTracker documentTracker)); Assert.Same(expectedDocumentTracker, documentTracker); } @@ -114,7 +136,7 @@ namespace Microsoft.VisualStudio.Editor.Razor } [Fact] - public void EnsureTextBufferInitialized_StoresParser() + public void TryInitializeTextBuffer_StoresParser_ReturnsTrue() { // Arrange var expectedParser = Mock.Of(); @@ -122,29 +144,31 @@ namespace Microsoft.VisualStudio.Editor.Razor var textBuffer = Mock.Of(b => b.ContentType == RazorCoreContentType && b.Properties == new PropertyCollection()); // Act - factoryService.EnsureTextBufferInitialized(textBuffer); + var result = factoryService.TryInitializeTextBuffer(textBuffer); // Assert + Assert.True(result); Assert.True(textBuffer.Properties.TryGetProperty(typeof(VisualStudioRazorParser), out VisualStudioRazorParser parser)); Assert.Same(expectedParser, parser); } [Fact] - public void EnsureTextBufferInitialized_OnlyStoresParserOnTextBufferOnce() + public void TryInitializeTextBuffer_OnlyStoresParserOnTextBufferOnce_ReturnsTrue() { // Arrange var factoryService = CreateFactoryService(); var textBuffer = Mock.Of(b => b.ContentType == RazorCoreContentType && b.Properties == new PropertyCollection()); - factoryService.EnsureTextBufferInitialized(textBuffer); + factoryService.TryInitializeTextBuffer(textBuffer); var expectedParser = textBuffer.Properties[typeof(VisualStudioRazorParser)]; // Create a second factory service so it generates a different parser factoryService = CreateFactoryService(); // Act - factoryService.EnsureTextBufferInitialized(textBuffer); + var result = factoryService.TryInitializeTextBuffer(textBuffer); // Assert + Assert.True(result); Assert.True(textBuffer.Properties.TryGetProperty(typeof(VisualStudioRazorParser), out VisualStudioRazorParser parser)); Assert.Same(expectedParser, parser); } @@ -181,7 +205,7 @@ namespace Microsoft.VisualStudio.Editor.Razor } [Fact] - public void EnsureTextBufferInitialized_StoresSmartIndenter() + public void TryInitializeTextBuffer_StoresSmartIndenter_ReturnsTrue() { // Arrange var expectedSmartIndenter = Mock.Of(); @@ -189,29 +213,31 @@ namespace Microsoft.VisualStudio.Editor.Razor var textBuffer = Mock.Of(b => b.ContentType == RazorCoreContentType && b.Properties == new PropertyCollection()); // Act - factoryService.EnsureTextBufferInitialized(textBuffer); + var result = factoryService.TryInitializeTextBuffer(textBuffer); // Assert + Assert.True(result); Assert.True(textBuffer.Properties.TryGetProperty(typeof(BraceSmartIndenter), out BraceSmartIndenter smartIndenter)); Assert.Same(expectedSmartIndenter, smartIndenter); } [Fact] - public void EnsureTextBufferInitialized_OnlyStoresSmartIndenterOnTextBufferOnce() + public void TryInitializeTextBuffer_OnlyStoresSmartIndenterOnTextBufferOnce_ReturnsTrue() { // Arrange var factoryService = CreateFactoryService(); var textBuffer = Mock.Of(b => b.ContentType == RazorCoreContentType && b.Properties == new PropertyCollection()); - factoryService.EnsureTextBufferInitialized(textBuffer); + factoryService.TryInitializeTextBuffer(textBuffer); var expectedSmartIndenter = textBuffer.Properties[typeof(BraceSmartIndenter)]; // Create a second factory service so it generates a different smart indenter factoryService = CreateFactoryService(); // Act - factoryService.EnsureTextBufferInitialized(textBuffer); + var result = factoryService.TryInitializeTextBuffer(textBuffer); // Assert + Assert.True(result); Assert.True(textBuffer.Properties.TryGetProperty(typeof(BraceSmartIndenter), out BraceSmartIndenter smartIndenter)); Assert.Same(expectedSmartIndenter, smartIndenter); } @@ -228,7 +254,20 @@ namespace Microsoft.VisualStudio.Editor.Razor var documentTrackerFactory = Mock.Of(f => f.Create(It.IsAny()) == documentTracker); var parserFactory = Mock.Of(f => f.Create(It.IsAny()) == parser); var smartIndenterFactory = Mock.Of(f => f.Create(It.IsAny()) == smartIndenter); - var factoryService = new DefaultRazorEditorFactoryService(documentTrackerFactory, parserFactory, smartIndenterFactory); + + var services = TestServices.Create(new ILanguageService[] + { + documentTrackerFactory, + parserFactory, + smartIndenterFactory + }); + + var workspace = TestWorkspace.Create(services); + var workspaceAccessor = new Mock(); + workspaceAccessor.Setup(p => p.TryGetWorkspace(It.IsAny(), out workspace)) + .Returns(true); + + var factoryService = new DefaultRazorEditorFactoryService(workspaceAccessor.Object); return factoryService; } diff --git a/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/DefaultVisualStudioWorkspaceAccessorTest.cs b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/DefaultVisualStudioWorkspaceAccessorTest.cs new file mode 100644 index 0000000000..95a0cb7105 --- /dev/null +++ b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/DefaultVisualStudioWorkspaceAccessorTest.cs @@ -0,0 +1,150 @@ +// 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.ObjectModel; +using Microsoft.CodeAnalysis; +using Microsoft.VisualStudio.Editor.Razor; +using Microsoft.VisualStudio.Text; +using Microsoft.VisualStudio.Text.Projection; +using Moq; +using Xunit; + +namespace Microsoft.VisualStudio.LanguageServices.Razor +{ + public class DefaultVisualStudioWorkspaceAccessorTest + { + [Fact] + public void TryGetWorkspace_CanGetWorkspaceFromProjectionBuffersOnly() + { + // Arrange + var textBuffer = Mock.Of(); + var workspaceAccessor = new TestWorkspaceAccessor(true, false); + + // Act + var result = workspaceAccessor.TryGetWorkspace(textBuffer, out var workspace); + + // Assert + Assert.True(result); + } + + [Fact] + public void TryGetWorkspace_CanGetWorkspaceFromBuffersInHierarchyOnly() + { + // Arrange + var textBuffer = Mock.Of(); + var workspaceAccessor = new TestWorkspaceAccessor(false, true); + + // Act + var result = workspaceAccessor.TryGetWorkspace(textBuffer, out var workspace); + + // Assert + Assert.True(result); + } + + [Fact] + public void TryGetWorkspace_CanGetWorkspaceFromBuffersInHierarchyOrProjectionBuffers() + { + // Arrange + var textBuffer = Mock.Of(); + var workspaceAccessor = new TestWorkspaceAccessor(true, true); + + // Act + var result = workspaceAccessor.TryGetWorkspace(textBuffer, out var workspace); + + // Assert + Assert.True(result); + } + + [Fact] + public void TryGetWorkspaceFromProjectionBuffer_NoProjectionBuffer_ReturnsFalse() + { + // Arrange + var bufferGraph = new Mock(); + bufferGraph.Setup(graph => graph.GetTextBuffers(It.IsAny>())) + .Returns>(predicate => new Collection()); + var bufferGraphService = new Mock(); + bufferGraphService.Setup(service => service.CreateBufferGraph(It.IsAny())) + .Returns(bufferGraph.Object); + var workspaceAccessor = new DefaultVisualStudioWorkspaceAccessor(bufferGraphService.Object, Mock.Of(), TestWorkspace.Create()); + var textBuffer = Mock.Of(); + + // Act + var result = workspaceAccessor.TryGetWorkspaceFromProjectionBuffer(textBuffer, out var workspace); + + // Assert + Assert.False(result); + } + + [Fact] + public void TryGetWorkspaceFromHostProject_NoHostProject_ReturnsFalse() + { + // Arrange + var workspaceAccessor = new DefaultVisualStudioWorkspaceAccessor(Mock.Of(), Mock.Of(), TestWorkspace.Create()); + var textBuffer = Mock.Of(); + + // Act + var result = workspaceAccessor.TryGetWorkspaceFromHostProject(textBuffer, out var workspace); + + // Assert + Assert.False(result); + } + + [Fact] + public void TryGetWorkspaceFromHostProject_HasHostProject_ReturnsTrueWithDefaultWorkspace() + { + // Arrange + var textBuffer = Mock.Of(); + var projectService = Mock.Of(service => service.GetHostProject(textBuffer) == new object()); + var defaultWorkspace = TestWorkspace.Create(); + var workspaceAccessor = new DefaultVisualStudioWorkspaceAccessor(Mock.Of(), projectService, defaultWorkspace); + + // Act + var result = workspaceAccessor.TryGetWorkspaceFromHostProject(textBuffer, out var workspace); + + // Assert + Assert.True(result); + Assert.Same(defaultWorkspace, workspace); + } + + private class TestWorkspaceAccessor : DefaultVisualStudioWorkspaceAccessor + { + private readonly bool _canGetWorkspaceFromProjectionBuffer; + private readonly bool _canGetWorkspaceFromHostProject; + + internal TestWorkspaceAccessor(bool canGetWorkspaceFromProjectionBuffer, bool canGetWorkspaceFromHostProject) : + base( + Mock.Of(), + Mock.Of(), + TestWorkspace.Create()) + { + _canGetWorkspaceFromProjectionBuffer = canGetWorkspaceFromProjectionBuffer; + _canGetWorkspaceFromHostProject = canGetWorkspaceFromHostProject; + } + + internal override bool TryGetWorkspaceFromProjectionBuffer(ITextBuffer textBuffer, out Workspace workspace) + { + if (_canGetWorkspaceFromProjectionBuffer) + { + workspace = TestWorkspace.Create(); + return true; + } + + workspace = null; + return false; + } + + internal override bool TryGetWorkspaceFromHostProject(ITextBuffer textBuffer, out Workspace workspace) + { + if (_canGetWorkspaceFromHostProject) + { + workspace = TestWorkspace.Create(); + return true; + } + + workspace = null; + return false; + } + } + } +}