diff --git a/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/Properties/AssemblyInfo.cs b/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/Properties/AssemblyInfo.cs index 6a4431ff38..6be3a1f170 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/Properties/AssemblyInfo.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/Properties/AssemblyInfo.cs @@ -4,3 +4,4 @@ using System.Runtime.CompilerServices; [assembly: InternalsVisibleTo("Microsoft.AspNetCore.Mvc.Razor.Extensions.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] +[assembly: InternalsVisibleTo("Microsoft.VisualStudio.Editor.Razor, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/TagHelperResolver.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/TagHelperResolver.cs index 9c278133b2..d79ed4fab2 100644 --- a/src/Microsoft.CodeAnalysis.Razor.Workspaces/TagHelperResolver.cs +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/TagHelperResolver.cs @@ -9,8 +9,6 @@ namespace Microsoft.CodeAnalysis.Razor { internal abstract class TagHelperResolver : ILanguageService { - public abstract TagHelperResolutionResult GetTagHelpers(Compilation compilation); - public abstract Task GetTagHelpersAsync(Project project, CancellationToken cancellationToken); } } diff --git a/src/Microsoft.CodeAnalysis.Remote.Razor/DefaultTagHelperResolver.cs b/src/Microsoft.CodeAnalysis.Remote.Razor/DefaultTagHelperResolver.cs index ddd8600b1f..4aca5c8f7e 100644 --- a/src/Microsoft.CodeAnalysis.Remote.Razor/DefaultTagHelperResolver.cs +++ b/src/Microsoft.CodeAnalysis.Remote.Razor/DefaultTagHelperResolver.cs @@ -19,7 +19,7 @@ namespace Microsoft.CodeAnalysis.Remote.Razor public bool DesignTime { get; } - public override TagHelperResolutionResult GetTagHelpers(Compilation compilation) + private TagHelperResolutionResult GetTagHelpers(Compilation compilation) { var descriptors = new List(); diff --git a/src/Microsoft.VisualStudio.Editor.Razor/DefaultTagHelperResolver.cs b/src/Microsoft.VisualStudio.Editor.Razor/DefaultTagHelperResolver.cs new file mode 100644 index 0000000000..ab49d1e27d --- /dev/null +++ b/src/Microsoft.VisualStudio.Editor.Razor/DefaultTagHelperResolver.cs @@ -0,0 +1,60 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc.Razor.Extensions; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Razor; + +namespace Microsoft.VisualStudio.Editor.Razor +{ + internal class DefaultTagHelperResolver : TagHelperResolver + { + // Hack for testability. The view component visitor will normally just no op if we're not referencing + // an appropriate version of MVC. + internal bool ForceEnableViewComponentDiscovery { get; set; } + + public override async Task GetTagHelpersAsync(Project project, CancellationToken cancellationToken) + { + if (project == null) + { + throw new ArgumentNullException(nameof(project)); + } + + var compilation = await project.GetCompilationAsync(cancellationToken).ConfigureAwait(false); + var result = GetTagHelpers(compilation); + return result; + } + + // Internal for testing + internal TagHelperResolutionResult GetTagHelpers(Compilation compilation) + { + var descriptors = new List(); + + var providers = new ITagHelperDescriptorProvider[] + { + new DefaultTagHelperDescriptorProvider() { DesignTime = true, }, + new ViewComponentTagHelperDescriptorProvider() { ForceEnabled = ForceEnableViewComponentDiscovery }, + }; + + var results = new List(); + var context = TagHelperDescriptorProviderContext.Create(results); + context.SetCompilation(compilation); + + for (var i = 0; i < providers.Length; i++) + { + var provider = providers[i]; + provider.Execute(context); + } + + var diagnostics = new List(); + var resolutionResult = new TagHelperResolutionResult(results, diagnostics); + + return resolutionResult; + } + } +} diff --git a/src/Microsoft.VisualStudio.Editor.Razor/DefaultTagHelperResolverFactory.cs b/src/Microsoft.VisualStudio.Editor.Razor/DefaultTagHelperResolverFactory.cs new file mode 100644 index 0000000000..50e2e96179 --- /dev/null +++ b/src/Microsoft.VisualStudio.Editor.Razor/DefaultTagHelperResolverFactory.cs @@ -0,0 +1,20 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Composition; +using Microsoft.CodeAnalysis.Host; +using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.CodeAnalysis.Razor; + +namespace Microsoft.VisualStudio.Editor.Razor +{ + [Shared] + [ExportLanguageServiceFactory(typeof(TagHelperResolver), RazorLanguage.Name, ServiceLayer.Default)] + internal class DefaultTagHelperResolverFactory : ILanguageServiceFactory + { + public ILanguageService CreateLanguageService(HostLanguageServices languageServices) + { + return new DefaultTagHelperResolver(); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/DefaultTagHelperResolver.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/OOPTagHelperResolver.cs similarity index 59% rename from src/Microsoft.VisualStudio.LanguageServices.Razor/DefaultTagHelperResolver.cs rename to src/Microsoft.VisualStudio.LanguageServices.Razor/OOPTagHelperResolver.cs index 0090d056d3..857cce5557 100644 --- a/src/Microsoft.VisualStudio.LanguageServices.Razor/DefaultTagHelperResolver.cs +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/OOPTagHelperResolver.cs @@ -2,27 +2,30 @@ // 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.Threading; using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc.Razor.Extensions; -using Microsoft.AspNetCore.Razor.Language; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Razor; +using Microsoft.VisualStudio.Editor.Razor; using Newtonsoft.Json; using Newtonsoft.Json.Linq; namespace Microsoft.VisualStudio.LanguageServices.Razor { - internal class DefaultTagHelperResolver : TagHelperResolver + internal class OOPTagHelperResolver : TagHelperResolver { - private readonly ErrorReporter _errorReporter; + private readonly DefaultTagHelperResolver _defaultResolver; private readonly Workspace _workspace; - public DefaultTagHelperResolver(ErrorReporter errorReporter, Workspace workspace) + public OOPTagHelperResolver(Workspace workspace) { - _errorReporter = errorReporter; + if (workspace == null) + { + throw new ArgumentNullException(nameof(workspace)); + } + _workspace = workspace; + _defaultResolver = new DefaultTagHelperResolver(); } public override async Task GetTagHelpersAsync(Project project, CancellationToken cancellationToken) @@ -34,7 +37,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor try { - TagHelperResolutionResult result; + TagHelperResolutionResult result = null; // We're being defensive here because the OOP host can return null for the client/session/operation // when it's disconnected (user stops the process). @@ -51,24 +54,20 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor cancellationToken).ConfigureAwait(false); result = GetTagHelperResolutionResult(jsonObject); - - if (result != null) - { - return result; - } } } } - // The OOP host is turned off, so let's do this in process. - var compilation = await project.GetCompilationAsync(cancellationToken).ConfigureAwait(false); - result = GetTagHelpers(compilation); + if (result == null) + { + // Was unable to get tag helpers OOP, fallback to default behavior. + result = await _defaultResolver.GetTagHelpersAsync(project, cancellationToken); + } + return result; } catch (Exception exception) { - _errorReporter.ReportError(exception, project); - throw new InvalidOperationException( Resources.FormatUnexpectedException( typeof(DefaultTagHelperResolver).FullName, @@ -77,32 +76,6 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor } } - public override TagHelperResolutionResult GetTagHelpers(Compilation compilation) - { - var descriptors = new List(); - - var providers = new ITagHelperDescriptorProvider[] - { - new DefaultTagHelperDescriptorProvider() { DesignTime = true, }, - new ViewComponentTagHelperDescriptorProvider(), - }; - - var results = new List(); - var context = TagHelperDescriptorProviderContext.Create(results); - context.SetCompilation(compilation); - - for (var i = 0; i < providers.Length; i++) - { - var provider = providers[i]; - provider.Execute(context); - } - - var diagnostics = new List(); - var resolutionResult = new TagHelperResolutionResult(results, diagnostics); - - return resolutionResult; - } - private TagHelperResolutionResult GetTagHelperResolutionResult(JObject jsonObject) { var serializer = new JsonSerializer(); diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/DefaultTagHelperResolverFactory.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/OOPTagHelperResolverFactory.cs similarity index 68% rename from src/Microsoft.VisualStudio.LanguageServices.Razor/DefaultTagHelperResolverFactory.cs rename to src/Microsoft.VisualStudio.LanguageServices.Razor/OOPTagHelperResolverFactory.cs index d17beb9151..d3265f7778 100644 --- a/src/Microsoft.VisualStudio.LanguageServices.Razor/DefaultTagHelperResolverFactory.cs +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/OOPTagHelperResolverFactory.cs @@ -1,20 +1,21 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using System.ComponentModel.Composition; +using System.Composition; using Microsoft.CodeAnalysis.Host; using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.Razor; namespace Microsoft.VisualStudio.LanguageServices.Razor { - [ExportLanguageServiceFactory(typeof(TagHelperResolver), RazorLanguage.Name, ServiceLayer.Default)] - internal class DefaultTagHelperResolverFactory : ILanguageServiceFactory + [Shared] + [ExportLanguageServiceFactory(typeof(TagHelperResolver), RazorLanguage.Name, ServiceLayer.Host)] + internal class OOPTagHelperResolverFactory : ILanguageServiceFactory { public ILanguageService CreateLanguageService(HostLanguageServices languageServices) { var workspace = languageServices.WorkspaceServices.Workspace; - return new DefaultTagHelperResolver(workspace.Services.GetRequiredService(), workspace); + return new OOPTagHelperResolver(workspace); } } } \ No newline at end of file diff --git a/test/Microsoft.VisualStudio.Editor.Razor.Test/DefaultTagHelperResolverTest.cs b/test/Microsoft.VisualStudio.Editor.Razor.Test/DefaultTagHelperResolverTest.cs new file mode 100644 index 0000000000..4d872b8b6f --- /dev/null +++ b/test/Microsoft.VisualStudio.Editor.Razor.Test/DefaultTagHelperResolverTest.cs @@ -0,0 +1,63 @@ +// 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.Reflection; +using Microsoft.AspNetCore.Razor.TagHelpers; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Xunit; + +namespace Microsoft.VisualStudio.Editor.Razor +{ + public class DefaultTagHelperResolverTest + { + private static readonly Assembly Assembly = typeof(DefaultTagHelperResolverTest).GetTypeInfo().Assembly; + + [Fact] + public void GetTagHelpers_DiscoversViewComponentTagHelpers() + { + // Arrange + var code = @" +public class TestViewComponent +{ + public string Invoke(string foo, string bar) => null; +}"; + var syntaxTree = CSharpSyntaxTree.ParseText(code); + var compilation = TestCompilation.Create(Assembly, syntaxTree); + var tagHelperResolver = new DefaultTagHelperResolver() + { + ForceEnableViewComponentDiscovery = true + }; + + // Act + var result = tagHelperResolver.GetTagHelpers(compilation); + + // Assert + Assert.Empty(result.Diagnostics); + Assert.Equal(1, result.Descriptors.Count); + } + + [Fact] + public void GetTagHelpers_DiscoversTagHelpers() + { + // Arrange + var code = $@" +public class TestTagHelper : {typeof(TagHelper).FullName} +{{ +}}"; + var syntaxTree = CSharpSyntaxTree.ParseText(code); + var compilation = TestCompilation.Create(Assembly, syntaxTree); + var tagHelperResolver = new DefaultTagHelperResolver() + { + ForceEnableViewComponentDiscovery = true + }; + + // Act + var result = tagHelperResolver.GetTagHelpers(compilation); + + // Assert + Assert.Empty(result.Diagnostics); + Assert.Equal(1, result.Descriptors.Count); + } + } +} diff --git a/test/Microsoft.VisualStudio.Editor.Razor.Test/Microsoft.VisualStudio.Editor.Razor.Test.csproj b/test/Microsoft.VisualStudio.Editor.Razor.Test/Microsoft.VisualStudio.Editor.Razor.Test.csproj index 7e010cf2a9..bd3366db65 100644 --- a/test/Microsoft.VisualStudio.Editor.Razor.Test/Microsoft.VisualStudio.Editor.Razor.Test.csproj +++ b/test/Microsoft.VisualStudio.Editor.Razor.Test/Microsoft.VisualStudio.Editor.Razor.Test.csproj @@ -2,13 +2,19 @@ net461 + true + + + + + diff --git a/test/Microsoft.VisualStudio.Editor.Razor.Test/xunit.runner.json b/test/Microsoft.VisualStudio.Editor.Razor.Test/xunit.runner.json new file mode 100644 index 0000000000..fcf172c8fc --- /dev/null +++ b/test/Microsoft.VisualStudio.Editor.Razor.Test/xunit.runner.json @@ -0,0 +1,4 @@ +{ + "methodDisplay": "method", + "shadowCopy": false +} \ No newline at end of file