Re-rework LspEditorFeatureDetector

My previous work on LspEditorFeatureDetector introduced a significant regression by removing the code that checked project capabilities. When a razor document is opened, the LspEditorFeatureDetector is queried to see if the LSP editor is enabled (i.e. the user hasn't enabled the legacy editor instead), and to see whether the project that the document belongs to supports the LSP editor. In the case of .NET Framework projects, the LSP editor can't ever be used, and my prior change broke that. (Many thanks to @alexgav for fixing my mistake!)

Giving the situation a bit more thought, I've reworked LspEditorFeatureDetector again to simplify the code and fix a different issue where we would start checking project capabilities where we hadn't before.
This commit is contained in:
Dustin Campbell 2024-07-01 15:52:49 -07:00
Родитель 47e4957042
Коммит 6cd5bd4ba6
15 изменённых файлов: 224 добавлений и 290 удалений

Просмотреть файл

@ -1,32 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT license. See License.txt in the project root for license information.
using System.Collections.Generic;
using System.ComponentModel.Composition;
namespace Microsoft.VisualStudio.Razor;
[Export(typeof(IAggregateProjectCapabilityResolver))]
internal sealed class AggregateProjectCapabilityResolver : IAggregateProjectCapabilityResolver
{
private readonly IEnumerable<IProjectCapabilityResolver> _projectCapabilityResolvers;
[ImportingConstructor]
public AggregateProjectCapabilityResolver([ImportMany] IEnumerable<IProjectCapabilityResolver> projectCapabilityResolvers)
{
_projectCapabilityResolvers = projectCapabilityResolvers;
}
public bool HasCapability(string documentFilePath, object project, string capability)
{
foreach (var capabilityResolver in _projectCapabilityResolvers)
{
if (capabilityResolver.HasCapability(documentFilePath, project, capability))
{
return true;
}
}
return false;
}
}

Просмотреть файл

@ -201,7 +201,7 @@ internal class RazorDynamicFileInfoProvider : IRazorDynamicFileInfoProviderInter
throw new ArgumentNullException(nameof(documentFilePath));
}
if (_lspEditorFeatureDetector.IsLspEditorEnabledAndAvailable(documentFilePath))
if (_lspEditorFeatureDetector.IsLspEditorEnabled())
{
return;
}

Просмотреть файл

@ -1,7 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT license. See License.txt in the project root for license information.
namespace Microsoft.VisualStudio.Razor;
// For MEF export and unit tests
internal interface IAggregateProjectCapabilityResolver : IProjectCapabilityResolver { }

Просмотреть файл

@ -5,7 +5,16 @@ namespace Microsoft.VisualStudio.Razor;
internal interface ILspEditorFeatureDetector
{
bool IsLspEditorEnabledAndAvailable(string documentFilePath);
/// <summary>
/// Determines whether the LSP editor is enabled. This returns <see langword="true"/>
/// if the legacy editor has <i>not</i> been enabled via the feature flag or tools/options.
/// </summary>
bool IsLspEditorEnabled();
/// <summary>
/// Determines whether the LSP editor is supported by the given document.
/// </summary>
bool IsLspEditorSupported(string documentFilePath);
/// <summary>
/// A remote client is a LiveShare guest or a Codespaces instance

Просмотреть файл

@ -5,5 +5,8 @@ namespace Microsoft.VisualStudio.Razor;
internal interface IProjectCapabilityResolver
{
bool HasCapability(string documentFilePath, object project, string capability);
/// <summary>
/// Determines whether the project associated with the specified document has the given <paramref name="capability"/>.
/// </summary>
bool ResolveCapability(string capability, string documentFilePath);
}

Просмотреть файл

@ -9,12 +9,10 @@ namespace Microsoft.VisualStudio.Razor.LanguageClient;
[FileExtension(RazorLSPConstants.CSHTMLFileExtension)]
[Name(nameof(CSHTMLFilePathToContentTypeProvider))]
[Export(typeof(IFilePathToContentTypeProvider))]
internal class CSHTMLFilePathToContentTypeProvider : RazorFilePathToContentTypeProviderBase
[method: ImportingConstructor]
internal class CSHTMLFilePathToContentTypeProvider(
IContentTypeRegistryService contentTypeRegistryService,
ILspEditorFeatureDetector lspEditorFeatureDetector)
: RazorFilePathToContentTypeProviderBase(contentTypeRegistryService, lspEditorFeatureDetector)
{
[ImportingConstructor]
public CSHTMLFilePathToContentTypeProvider(
IContentTypeRegistryService contentTypeRegistryService,
ILspEditorFeatureDetector lspEditorFeatureDetector) : base(contentTypeRegistryService, lspEditorFeatureDetector)
{
}
}

Просмотреть файл

@ -9,12 +9,10 @@ namespace Microsoft.VisualStudio.Razor.LanguageClient;
[FileExtension(RazorLSPConstants.RazorFileExtension)]
[Name(nameof(RazorFilePathToContentTypeProvider))]
[Export(typeof(IFilePathToContentTypeProvider))]
internal class RazorFilePathToContentTypeProvider : RazorFilePathToContentTypeProviderBase
[method: ImportingConstructor]
internal class RazorFilePathToContentTypeProvider(
IContentTypeRegistryService contentTypeRegistryService,
ILspEditorFeatureDetector lspEditorFeatureDetector)
: RazorFilePathToContentTypeProviderBase(contentTypeRegistryService, lspEditorFeatureDetector)
{
[ImportingConstructor]
public RazorFilePathToContentTypeProvider(
IContentTypeRegistryService contentTypeRegistryService,
ILspEditorFeatureDetector lspEditorFeatureDetector) : base(contentTypeRegistryService, lspEditorFeatureDetector)
{
}
}

Просмотреть файл

@ -1,7 +1,6 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT license. See License.txt in the project root for license information.
using System;
using System.Diagnostics.CodeAnalysis;
using Microsoft.VisualStudio.Utilities;
@ -16,23 +15,14 @@ internal abstract class RazorFilePathToContentTypeProviderBase : IFilePathToCont
IContentTypeRegistryService contentTypeRegistryService,
ILspEditorFeatureDetector lspEditorFeatureDetector)
{
if (contentTypeRegistryService is null)
{
throw new ArgumentNullException(nameof(contentTypeRegistryService));
}
if (lspEditorFeatureDetector is null)
{
throw new ArgumentNullException(nameof(lspEditorFeatureDetector));
}
_contentTypeRegistryService = contentTypeRegistryService;
_lspEditorFeatureDetector = lspEditorFeatureDetector;
}
public bool TryGetContentTypeForFilePath(string filePath, [NotNullWhen(true)] out IContentType? contentType)
{
if (_lspEditorFeatureDetector.IsLspEditorEnabledAndAvailable(filePath))
if (_lspEditorFeatureDetector.IsLspEditorEnabled() &&
_lspEditorFeatureDetector.IsLspEditorSupported(filePath))
{
contentType = _contentTypeRegistryService.GetContentType(RazorConstants.RazorLSPContentTypeName);
return true;

Просмотреть файл

@ -1,6 +1,7 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT license. See License.txt in the project root for license information.
using System.Diagnostics.CodeAnalysis;
using Microsoft.VisualStudio.LiveShare;
namespace Microsoft.VisualStudio.Razor.LiveShare.Guest;
@ -8,5 +9,7 @@ namespace Microsoft.VisualStudio.Razor.LiveShare.Guest;
internal interface ILiveShareSessionAccessor
{
CollaborationSession? Session { get; }
[MemberNotNullWhen(true, nameof(Session))]
bool IsGuestSessionActive { get; }
}

Просмотреть файл

@ -1,44 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT license. See License.txt in the project root for license information.
using System.ComponentModel.Composition;
using System.Threading;
using Microsoft.VisualStudio.Razor.LiveShare.Guest;
using Microsoft.VisualStudio.Threading;
namespace Microsoft.VisualStudio.Razor.LiveShare;
[Export(typeof(IProjectCapabilityResolver))]
[method: ImportingConstructor]
internal class LiveShareProjectCapabilityResolver(
ILiveShareSessionAccessor sessionAccessor,
JoinableTaskContext joinableTaskContext) : IProjectCapabilityResolver
{
private readonly ILiveShareSessionAccessor _sessionAccessor = sessionAccessor;
private readonly JoinableTaskFactory _joinableTaskFactory = joinableTaskContext.Factory;
public bool HasCapability(string documentFilePath, object project, string capability)
{
if (!_sessionAccessor.IsGuestSessionActive)
{
// We don't provide capabilities for non-guest sessions.
return false;
}
var remoteHasCapability = RemoteHasCapability(documentFilePath, capability);
return remoteHasCapability;
}
private bool RemoteHasCapability(string documentMoniker, string capability)
{
// On a guest box. The project hierarchy is not fully populated. We need to ask the host machine
// questions on hierarchy capabilities.
return _joinableTaskFactory.Run(async () =>
{
var remoteHierarchyService = await _sessionAccessor.Session!.GetRemoteServiceAsync<IRemoteHierarchyService>(nameof(IRemoteHierarchyService), CancellationToken.None).ConfigureAwait(false);
var documentMonikerUri = _sessionAccessor.Session.ConvertLocalPathToSharedUri(documentMoniker);
var hasCapability = await remoteHierarchyService.HasCapabilityAsync(documentMonikerUri, capability, CancellationToken.None).ConfigureAwait(false);
return hasCapability;
});
}
}

Просмотреть файл

@ -9,8 +9,6 @@ using Microsoft.Internal.VisualStudio.Shell.Interop;
using Microsoft.VisualStudio.Razor.Extensions;
using Microsoft.VisualStudio.Razor.Logging;
using Microsoft.VisualStudio.Settings;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Shell.Interop;
using Microsoft.VisualStudio.Threading;
namespace Microsoft.VisualStudio.Razor;
@ -19,61 +17,31 @@ namespace Microsoft.VisualStudio.Razor;
internal sealed class LspEditorFeatureDetector : ILspEditorFeatureDetector, IDisposable
{
private readonly IUIContextService _uiContextService;
private readonly IProjectCapabilityResolver _projectCapabilityResolver;
private readonly JoinableTaskFactory _jtf;
private readonly CancellationTokenSource _disposeTokenSource;
private readonly IAggregateProjectCapabilityResolver _projectCapabilityResolver;
private readonly Lazy<IVsUIShellOpenDocument> _vsUIShellOpenDocument;
private readonly AsyncLazy<bool> _lazyUseLegacyEditorTask;
private readonly RazorActivityLog _activityLog;
private readonly CancellationTokenSource _disposeTokenSource;
private readonly AsyncLazy<bool> _lazyLegacyEditorEnabled;
[ImportingConstructor]
public LspEditorFeatureDetector(
IAggregateProjectCapabilityResolver projectCapabilityResolver,
IVsService<SVsFeatureFlags, IVsFeatureFlags> vsFeatureFlagsService,
IVsService<SVsSettingsPersistenceManager, ISettingsManager> vsSettingsManagerService,
IUIContextService uiContextService,
IProjectCapabilityResolver projectCapabilityResolver,
JoinableTaskContext joinableTaskContext,
RazorActivityLog activityLog)
: this(
projectCapabilityResolver,
vsFeatureFlagsService,
vsSettingsManagerService,
uiContextService,
joinableTaskContext,
activityLog,
new Lazy<IVsUIShellOpenDocument>(() =>
{
// This method is first called by out IFilePathToContentTypeProvider.TryGetContentTypeForFilePath(...) implementations on UI thread.
ThreadHelper.ThrowIfNotOnUIThread();
var shellOpenDocument = (IVsUIShellOpenDocument)ServiceProvider.GlobalProvider.GetService(typeof(SVsUIShellOpenDocument));
Assumes.Present(shellOpenDocument);
return shellOpenDocument;
})
)
{ }
// Primarily for unit tests
public LspEditorFeatureDetector(
IAggregateProjectCapabilityResolver projectCapabilityResolver,
IVsService<SVsFeatureFlags, IVsFeatureFlags> vsFeatureFlagsService,
IVsService<SVsSettingsPersistenceManager, ISettingsManager> vsSettingsManagerService,
IUIContextService uiContextService,
JoinableTaskContext joinableTaskContext,
RazorActivityLog activityLog,
Lazy<IVsUIShellOpenDocument> vsUIShellOpenDocument)
{
_uiContextService = uiContextService;
_projectCapabilityResolver = projectCapabilityResolver;
_jtf = joinableTaskContext.Factory;
_activityLog = activityLog;
_disposeTokenSource = new();
_projectCapabilityResolver = projectCapabilityResolver;
_vsUIShellOpenDocument = vsUIShellOpenDocument;
_lazyUseLegacyEditorTask = new(() =>
_lazyLegacyEditorEnabled = new(() =>
ComputeUseLegacyEditorAsync(vsFeatureFlagsService, vsSettingsManagerService, activityLog, _disposeTokenSource.Token),
_jtf);
_activityLog = activityLog;
}
public void Dispose()
@ -100,7 +68,7 @@ internal sealed class LspEditorFeatureDetector : ILspEditorFeatureDetector, IDis
if (vsFeatureFlags.IsFeatureEnabled(WellKnownFeatureFlagNames.UseLegacyRazorEditor, defaultValue: false))
#pragma warning restore VSTHRD010 // Invoke single-threaded types on Main thread
{
activityLog.LogInfo($"Using Legacy Razor editor because the '{WellKnownFeatureFlagNames.UseLegacyRazorEditor}' feature flag is enabled.");
activityLog.LogInfo($"Using legacy editor because the '{WellKnownFeatureFlagNames.UseLegacyRazorEditor}' feature flag is enabled.");
return true;
}
@ -109,87 +77,62 @@ internal sealed class LspEditorFeatureDetector : ILspEditorFeatureDetector, IDis
if (useLegacyEditorSetting)
{
activityLog.LogInfo($"Using Legacy Razor editor because the '{WellKnownSettingNames.UseLegacyASPNETCoreEditor}' setting is set to true.");
activityLog.LogInfo($"Using legacy editor because the '{WellKnownSettingNames.UseLegacyASPNETCoreEditor}' setting is set to true.");
return true;
}
activityLog.LogInfo($"Using LSP Razor editor.");
activityLog.LogInfo($"Using LSP editor.");
return false;
}
/// <summary>
/// Checks that LSP editor is enabled via feature flag and tools/options setting, and available for document's project.
/// </summary>
/// <param name="documentFilePath">Document to check for project compatibility with LSP</param>
/// <returns>true if LSP editor is enabled and available for document's project</returns>
public bool IsLspEditorEnabledAndAvailable(string documentFilePath)
/// <inheritdoc/>
public bool IsLspEditorEnabled()
{
// This method is first called by out IFilePathToContentTypeProvider.TryGetContentTypeForFilePath(...) implementations.
// We call AsyncLazy<T>.GetValue() below to get the value. If the work hasn't yet completed, we guard against a hidden+
// This method is first called by our IFilePathToContentTypeProvider.TryGetContentTypeForFilePath(...) implementations.
// We call AsyncLazy<T>.GetValue() below to get the value. If the work hasn't yet completed, we guard against a hidden
// JTF.Run(...) on a background thread by asserting the UI thread.
if (!_lazyUseLegacyEditorTask.IsValueFactoryCompleted)
if (!_lazyLegacyEditorEnabled.IsValueFactoryCompleted)
{
_jtf.AssertUIThread();
}
var isLspEditorEnabled = !_lazyUseLegacyEditorTask.GetValue(_disposeTokenSource.Token);
if (!isLspEditorEnabled)
var useLegacyEditorEnabled = _lazyLegacyEditorEnabled.GetValue(_disposeTokenSource.Token);
if (useLegacyEditorEnabled)
{
_activityLog.LogInfo("Using Legacy editor because the option or feature flag was set to true");
_activityLog.LogInfo("Using legacy editor because the option or feature flag was set to true");
return false;
}
// Even if LSP editor is enabled via feature flag and tools/options, document's project might not support
// LSP editor (e.g. .Net Framework projects don't support LSP Razor editor)
if (!ContainingProjectSupportsLspEditor(documentFilePath))
{
// Current project hierarchy doesn't support the LSP Razor editor
_activityLog.LogInfo("Using Legacy editor because the current project does not support LSP Editor");
return false;
}
_activityLog.LogInfo("LSP Editor is enabled and available");
_activityLog.LogInfo("LSP editor is enabled.");
return true;
}
// NOTE: This code is needed for legacy Razor editor support in .Net Framework projects. Do not delete unless support for .Net Framework projects is discontinued.
private bool ContainingProjectSupportsLspEditor(string documentFilePath)
/// <inheritdoc/>
public bool IsLspEditorSupported(string documentFilePath)
{
var hr = _vsUIShellOpenDocument.Value.IsDocumentInAProject(documentFilePath, out var uiHierarchy, out _, out _, out _);
var hierarchy = uiHierarchy as IVsHierarchy;
if (!ErrorHandler.Succeeded(hr))
// Regardless of whether the LSP is enabled via the feature flag or tools/options, the document's project
// might not support it. For example, .NET Framework projects don't support the LSP Razor editor.
var useLegacyEditor = _projectCapabilityResolver.ResolveCapability(WellKnownProjectCapabilities.LegacyRazorEditor, documentFilePath);
if (useLegacyEditor)
{
_activityLog.LogWarning($"Project does not support LSP Editor because {nameof(_vsUIShellOpenDocument.Value.IsDocumentInAProject)} failed with exit code {hr}");
return false;
}
if (hierarchy is null)
{
_activityLog.LogWarning($"Project does not support LSP Editor because {nameof(hierarchy)} is null");
_activityLog.LogInfo($"'{documentFilePath}' does not support the LSP editor because it is associated with the '{WellKnownProjectCapabilities.LegacyRazorEditor}' capability.");
return false;
}
// We allow projects to specifically opt-out of the legacy Razor editor because there are legacy scenarios which would rely on behind-the-scenes
// opt-out mechanics to enable the .NET Core editor in non-.NET Core scenarios. Therefore, we need a similar mechanic to continue supporting
// those types of scenarios for the new .NET Core Razor editor.
if (_projectCapabilityResolver.HasCapability(documentFilePath, hierarchy, WellKnownProjectCapabilities.LegacyRazorEditor))
var supportsRazor = _projectCapabilityResolver.ResolveCapability(WellKnownProjectCapabilities.DotNetCoreCSharp, documentFilePath);
if (!supportsRazor)
{
_activityLog.LogInfo($"Project does not support LSP Editor because '{documentFilePath}' has Capability {WellKnownProjectCapabilities.LegacyRazorEditor}");
// CPS project that requires the legacy editor
_activityLog.LogInfo($"'{documentFilePath}' does not support the LSP editor because it is not associated with the '{WellKnownProjectCapabilities.DotNetCoreCSharp}' capability.");
return false;
}
if (_projectCapabilityResolver.HasCapability(documentFilePath, hierarchy, WellKnownProjectCapabilities.DotNetCoreCSharp))
{
// .NET Core project that supports C#
return true;
}
_activityLog.LogInfo($"Project {documentFilePath} does not support LSP Editor because it does not have the {WellKnownProjectCapabilities.DotNetCoreCSharp} capability.");
// Not a C# .NET Core project. This typically happens for legacy Razor scenarios
return false;
_activityLog.LogInfo($"LSP editor is supported for '{documentFilePath}'.");
return supportsRazor;
}
public bool IsRemoteClient()

Просмотреть файл

@ -0,0 +1,130 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT license. See License.txt in the project root for license information.
using System;
using System.ComponentModel.Composition;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor;
using Microsoft.CodeAnalysis.Razor.Logging;
using Microsoft.VisualStudio.Razor.Extensions;
using Microsoft.VisualStudio.Razor.LiveShare;
using Microsoft.VisualStudio.Razor.LiveShare.Guest;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Shell.Interop;
using Microsoft.VisualStudio.Threading;
namespace Microsoft.VisualStudio.Razor;
[Export(typeof(IProjectCapabilityResolver))]
internal sealed class ProjectCapabilityResolver : IProjectCapabilityResolver, IDisposable
{
private readonly ILiveShareSessionAccessor _liveShareSessionAccessor;
private readonly AsyncLazy<IVsUIShellOpenDocument> _lazyVsUIShellOpenDocument;
private readonly ILogger _logger;
private readonly JoinableTaskFactory _jtf;
private readonly CancellationTokenSource _disposeTokenSource;
[ImportingConstructor]
public ProjectCapabilityResolver(
ILiveShareSessionAccessor liveShareSessionAccessor,
IVsService<SVsUIShellOpenDocument, IVsUIShellOpenDocument> vsUIShellOpenDocumentService,
ILoggerFactory loggerFactory,
JoinableTaskContext joinableTaskContext)
{
_liveShareSessionAccessor = liveShareSessionAccessor;
_jtf = joinableTaskContext.Factory;
_logger = loggerFactory.GetOrCreateLogger<ProjectCapabilityResolver>();
_disposeTokenSource = new();
// IVsService<,> doesn't provide a synchronous GetValue(...) method, so we wrap it in an AsyncLazy<>.
_lazyVsUIShellOpenDocument = new(
() => vsUIShellOpenDocumentService.GetValueAsync(_disposeTokenSource.Token),
_jtf);
}
public void Dispose()
{
if (_disposeTokenSource.IsCancellationRequested)
{
return;
}
_disposeTokenSource.Cancel();
_disposeTokenSource.Dispose();
}
/// <inheritdoc/>
public bool ResolveCapability(string capability, string documentFilePath)
{
// If a LiveShare is currently active, we call into the host to resolve project capabilities.
// Otherwise, we use the project that contains documentFilePath to resolve capabilities.
return _liveShareSessionAccessor.IsGuestSessionActive
? LiveShareHostHasCapability(capability, documentFilePath)
: ContainingProjectHasCapability(capability, documentFilePath);
}
private bool LiveShareHostHasCapability(string capability, string documentFilePath)
{
Debug.Assert(_liveShareSessionAccessor.IsGuestSessionActive);
// Using JTF.Run(...) here isn't great, but this is how Razor's LiveShare implementation has
// always worked. It won't be called unless a LiveShare collaboration session is active.
return _jtf.Run(() => LiveShareHostHasCapabilityAsync(capability, documentFilePath, _disposeTokenSource.Token));
async Task<bool> LiveShareHostHasCapabilityAsync(string capability, string documentFilePath, CancellationToken cancellationToken)
{
// On a guest box. The project hierarchy is not fully populated. We need to ask the host machine
// questions about hierarchy capabilities.
var session = _liveShareSessionAccessor.Session.AssumeNotNull();
var remoteHierarchyService = await session
.GetRemoteServiceAsync<IRemoteHierarchyService>(nameof(IRemoteHierarchyService), cancellationToken)
.ConfigureAwait(false);
var documentFilePathUri = session.ConvertLocalPathToSharedUri(documentFilePath);
return await remoteHierarchyService
.HasCapabilityAsync(documentFilePathUri, capability, cancellationToken)
.ConfigureAwait(false);
}
}
private bool ContainingProjectHasCapability(string capability, string documentFilePath)
{
// This method is only ever called by our IFilePathToContentTypeProvider.TryGetContentTypeForFilePath(...) implementations.
// We call AsyncLazy<T>.GetValue() below to get the value. If the work hasn't yet completed, we guard against a hidden
// JTF.Run(...) on a background thread by asserting the UI thread.
_jtf.AssertUIThread();
var vsUIShellOpenDocument = _lazyVsUIShellOpenDocument.GetValue(_disposeTokenSource.Token);
var result = vsUIShellOpenDocument.IsDocumentInAProject(documentFilePath, out var vsHierarchy, out _, out _, out _);
if (!ErrorHandler.Succeeded(result))
{
_logger.LogWarning($"Project does not support LSP Editor because {nameof(IVsUIShellOpenDocument.IsDocumentInAProject)} failed with error code: {result:x8}");
return false;
}
try
{
return vsHierarchy.IsCapabilityMatch(capability);
}
catch (NotSupportedException)
{
// IsCapabilityMatch throws a NotSupportedException if it can't create a
// BooleanSymbolExpressionEvaluator COM object
return false;
}
catch (ObjectDisposedException)
{
// IsCapabilityMatch throws an ObjectDisposedException if the underlying hierarchy has been disposed
return false;
}
}
}

Просмотреть файл

@ -1,50 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT license. See License.txt in the project root for license information.
using System;
using System.ComponentModel.Composition;
using Microsoft.CodeAnalysis.Razor.Logging;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Shell.Interop;
namespace Microsoft.VisualStudio.Razor;
[Export(typeof(IProjectCapabilityResolver))]
[method: ImportingConstructor]
internal sealed class VisualStudioProjectCapabilityResolver(ILoggerFactory loggerFactory) : IProjectCapabilityResolver
{
private readonly ILogger _logger = loggerFactory.GetOrCreateLogger<VisualStudioProjectCapabilityResolver>();
public bool HasCapability(string documentFilePath, object project, string capability)
{
if (project is not IVsHierarchy vsHierarchy)
{
return false;
}
var localHasCapability = LocalHasCapability(vsHierarchy, capability);
return localHasCapability;
}
private bool LocalHasCapability(IVsHierarchy hierarchy, string capability)
{
try
{
var hasCapability = hierarchy.IsCapabilityMatch(capability);
return hasCapability;
}
catch (NotSupportedException)
{
// IsCapabilityMatch throws a NotSupportedException if it can't create a
// BooleanSymbolExpressionEvaluator COM object
_logger.LogWarning($"Could not resolve project capability for hierarchy due to NotSupportedException.");
return false;
}
catch (ObjectDisposedException)
{
// IsCapabilityMatch throws an ObjectDisposedException if the underlying hierarchy has been disposed
_logger.LogWarning($"Could not resolve project capability for hierarchy due to hierarchy being disposed.");
return false;
}
}
}

Просмотреть файл

@ -215,7 +215,10 @@ public class RazorContentTypeChangeListenerTest : ToolingTestBase
Mock.Get(textDocumentFactory).Setup(f => f.TryGetTextDocument(It.IsAny<ITextBuffer>(), out It.Ref<ITextDocument>.IsAny)).Returns(false);
lspDocumentManager ??= Mock.Of<TrackingLSPDocumentManager>(MockBehavior.Strict);
lspEditorFeatureDetector ??= Mock.Of<ILspEditorFeatureDetector>(detector => detector.IsLspEditorEnabledAndAvailable(It.IsAny<string>()) == true && detector.IsRemoteClient() == false, MockBehavior.Strict);
lspEditorFeatureDetector ??= Mock.Of<ILspEditorFeatureDetector>(detector =>
detector.IsLspEditorEnabled() == true &&
detector.IsLspEditorSupported(It.IsAny<string>()) == true &&
detector.IsRemoteClient() == false, MockBehavior.Strict);
fileToContentTypeService ??= Mock.Of<IFileToContentTypeService>(detector => detector.GetContentTypeForFilePath(It.IsAny<string>()) == _razorContentType, MockBehavior.Strict);
var textManager = new Mock<IVsTextManager2>(MockBehavior.Strict);
textManager.Setup(m => m.GetUserPreferences2(null, null, It.IsAny<LANGPREFERENCES2[]>(), null)).Returns(VSConstants.E_NOTIMPL);

Просмотреть файл

@ -1,13 +1,11 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT license. See License.txt in the project root for license information.
using System;
using Microsoft.AspNetCore.Razor;
using Microsoft.AspNetCore.Razor.Test.Common;
using Microsoft.AspNetCore.Razor.Test.Common.VisualStudio;
using Microsoft.CodeAnalysis.Razor.Logging;
using Microsoft.Internal.VisualStudio.Shell.Interop;
using Microsoft.VisualStudio.ProjectSystem.VS;
using Microsoft.VisualStudio.Razor.Logging;
using Microsoft.VisualStudio.Settings;
using Microsoft.VisualStudio.Shell;
@ -37,13 +35,13 @@ public class LspEditorFeatureDetectorTest(ITestOutputHelper testOutput) : Toolin
var featureDetector = CreateLspEditorFeatureDetector(legacyEditorFeatureFlag, legacyEditorSetting);
// Act
var result = featureDetector.IsLspEditorEnabledAndAvailable(@"c:\TestProject\TestFile.cshtml");
var result = featureDetector.IsLspEditorEnabled();
// Assert
Assert.Equal(expectedResult, result);
}
public static TheoryData<bool, bool, bool, bool, bool> IsLspEditorAvailableTestData { get; } = new()
public static TheoryData<bool, bool, bool, bool, bool> IsLspEditorEnabledAndSupportedTestData { get; } = new()
{
// legacyEditorFeatureFlag, legacyEditorSetting, hasLegacyRazorEditorCapability, hasDotNetCoreCSharpCapability, expectedResult
{ false, false, true, false, false }, // .Net Framework project - always non-LSP
@ -54,8 +52,8 @@ public class LspEditorFeatureDetectorTest(ITestOutputHelper testOutput) : Toolin
};
[UITheory]
[MemberData(nameof(IsLspEditorAvailableTestData))]
public void IsLspEditorAvailable(
[MemberData(nameof(IsLspEditorEnabledAndSupportedTestData))]
public void IsLspEditorEnabledAndSupported(
bool legacyEditorFeatureFlag,
bool legacyEditorSetting,
bool hasLegacyRazorEditorCapability,
@ -66,7 +64,8 @@ public class LspEditorFeatureDetectorTest(ITestOutputHelper testOutput) : Toolin
var featureDetector = CreateLspEditorFeatureDetector(legacyEditorFeatureFlag, legacyEditorSetting, hasLegacyRazorEditorCapability, hasDotNetCoreCSharpCapability);
// Act
var result = featureDetector.IsLspEditorEnabledAndAvailable(@"c:\TestProject\TestFile.cshtml");
var result = featureDetector.IsLspEditorEnabled() &&
featureDetector.IsLspEditorSupported(@"c:\TestProject\TestFile.cshtml");
// Assert
Assert.Equal(expectedResult, result);
@ -146,49 +145,25 @@ public class LspEditorFeatureDetectorTest(ITestOutputHelper testOutput) : Toolin
uiContextService ??= CreateUIContextService();
var featureDetector = new LspEditorFeatureDetector(
CreateAggregateProjectCapabilityResolver(hasLegacyRazorEditorCapability, hasDotNetCoreCSharpCapability),
CreateVsFeatureFlagsService(legacyEditorFeatureFlag),
CreateVsSettingsManagerService(legacyEditorSetting),
uiContextService,
CreateProjectCapabilityResolver(hasLegacyRazorEditorCapability, hasDotNetCoreCSharpCapability),
JoinableTaskContext,
CreateRazorActivityLog(),
CreateVSUIShellOpenDocument());
CreateRazorActivityLog());
AddDisposable(featureDetector);
return featureDetector;
}
private static IAggregateProjectCapabilityResolver CreateAggregateProjectCapabilityResolver(bool hasLegacyRazorEditorCapability, bool hasDotNetCoreCSharpCapability)
{
var aggregateProjectCapabilityResolverMock = new StrictMock<IAggregateProjectCapabilityResolver>();
aggregateProjectCapabilityResolverMock
.Setup(x => x.HasCapability(It.IsAny<string>(), It.IsAny<object>(), WellKnownProjectCapabilities.LegacyRazorEditor))
.Returns(hasLegacyRazorEditorCapability);
aggregateProjectCapabilityResolverMock
.Setup(x => x.HasCapability(It.IsAny<string>(), It.IsAny<object>(), WellKnownProjectCapabilities.DotNetCoreCSharp))
.Returns(hasDotNetCoreCSharpCapability);
return aggregateProjectCapabilityResolverMock.Object;
}
private static Lazy<IVsUIShellOpenDocument> CreateVSUIShellOpenDocument()
{
var vsUIShellOpenDocumentMock = new StrictMock<IVsUIShellOpenDocument>();
var hierarchy = new StrictMock<IVsUIHierarchy>().Object;
vsUIShellOpenDocumentMock
.Setup(x => x.IsDocumentInAProject(It.IsAny<string>(), out hierarchy, out It.Ref<uint>.IsAny, out It.Ref<OLE.Interop.IServiceProvider>.IsAny, out It.Ref<int>.IsAny))
.Returns(VSConstants.S_OK);
return new Lazy<IVsUIShellOpenDocument>(() => vsUIShellOpenDocumentMock.Object);
}
private static IVsService<SVsFeatureFlags, IVsFeatureFlags> CreateVsFeatureFlagsService(bool useLegacyEditor)
{
var vsFeatureFlagsMock = new StrictMock<IVsFeatureFlags>();
vsFeatureFlagsMock
.Setup(x => x.IsFeatureEnabled(WellKnownFeatureFlagNames.UseLegacyRazorEditor, It.IsAny<bool>()))
.Returns(useLegacyEditor);
return VsMocks.CreateVsService<SVsFeatureFlags, IVsFeatureFlags>(vsFeatureFlagsMock);
}
@ -220,6 +195,21 @@ public class LspEditorFeatureDetectorTest(ITestOutputHelper testOutput) : Toolin
return mock.Object;
}
private static IProjectCapabilityResolver CreateProjectCapabilityResolver(bool hasLegacyRazorEditorCapability, bool hasDotNetCoreCSharpCapability)
{
var projectCapabilityResolverMock = new StrictMock<IProjectCapabilityResolver>();
projectCapabilityResolverMock
.Setup(x => x.ResolveCapability(WellKnownProjectCapabilities.LegacyRazorEditor, It.IsAny<string>()))
.Returns(hasLegacyRazorEditorCapability);
projectCapabilityResolverMock
.Setup(x => x.ResolveCapability(WellKnownProjectCapabilities.DotNetCoreCSharp, It.IsAny<string>()))
.Returns(hasDotNetCoreCSharpCapability);
return projectCapabilityResolverMock.Object;
}
private RazorActivityLog CreateRazorActivityLog()
{
var vsActivityLogMock = new StrictMock<IVsActivityLog>();