diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index eafe3cd7ad..c3c60c45d9 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -6,9 +6,9 @@ 839cdfb0ecca5e0be3dbccd926e7651ef50fdf10 - + https://github.com/dotnet/source-build-reference-packages - ccd0927e3823fb178c7151594f5d2eaba81bba81 + 136e43e45e20bd58bf86eeabba0a0fa7e1a4b3ae diff --git a/eng/Versions.props b/eng/Versions.props index 281e8d7b83..904a29e3ea 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -49,7 +49,7 @@ 6.0.2-servicing.22064.6 6.0.1 - 10.0.0-alpha.1.24521.1 + 10.0.0-alpha.1.24530.1 9.0.0-beta.24516.2 1.0.0-beta.23475.1 1.0.0-beta.23475.1 diff --git a/eng/targets/Services.props b/eng/targets/Services.props index 63c92b28ef..72a31d7a3f 100644 --- a/eng/targets/Services.props +++ b/eng/targets/Services.props @@ -33,6 +33,7 @@ + diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorLanguageServer.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorLanguageServer.cs index a2178feaad..b82abc2936 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorLanguageServer.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorLanguageServer.cs @@ -130,7 +130,6 @@ internal partial class RazorLanguageServer : SystemTextJsonLanguageServer(); services.AddSingleton(); + + // Hover + services.AddHoverServices(); } // Other diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/ClientCapabilitiesExtensions.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/ClientCapabilitiesExtensions.cs new file mode 100644 index 0000000000..e3d51e5a63 --- /dev/null +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/ClientCapabilitiesExtensions.cs @@ -0,0 +1,31 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using System; + +namespace Microsoft.VisualStudio.LanguageServer.Protocol; + +internal static class ClientCapabilitiesExtensions +{ + public static MarkupKind GetMarkupKind(this ClientCapabilities clientCapabilities) + { + // If MarkDown is supported, we'll use that. + if (clientCapabilities.TextDocument?.Hover?.ContentFormat is MarkupKind[] contentFormat && + Array.IndexOf(contentFormat, MarkupKind.Markdown) >= 0) + { + return MarkupKind.Markdown; + } + + return MarkupKind.PlainText; + } + + public static bool SupportsMarkdown(this ClientCapabilities clientCapabilities) + { + return clientCapabilities.GetMarkupKind() == MarkupKind.Markdown; + } + + public static bool SupportsVisualStudioExtensions(this ClientCapabilities clientCapabilities) + { + return (clientCapabilities as VSInternalClientCapabilities)?.SupportsVisualStudioExtensions ?? false; + } +} diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Hover/HoverDisplayOptions.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Hover/HoverDisplayOptions.cs index a038979a05..6d56f33edd 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Hover/HoverDisplayOptions.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Hover/HoverDisplayOptions.cs @@ -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 Microsoft.VisualStudio.LanguageServer.Protocol; namespace Microsoft.CodeAnalysis.Razor.Hover; @@ -10,16 +9,8 @@ internal readonly record struct HoverDisplayOptions(MarkupKind MarkupKind, bool { public static HoverDisplayOptions From(ClientCapabilities clientCapabilities) { - var markupKind = MarkupKind.PlainText; - - // If MarkDown is supported, we'll use that. - if (clientCapabilities.TextDocument?.Hover?.ContentFormat is MarkupKind[] contentFormat && - Array.IndexOf(contentFormat, MarkupKind.Markdown) >= 0) - { - markupKind = MarkupKind.Markdown; - } - - var supportsVisualStudioExtensions = (clientCapabilities as VSInternalClientCapabilities)?.SupportsVisualStudioExtensions ?? false; + var markupKind = clientCapabilities.GetMarkupKind(); + var supportsVisualStudioExtensions = clientCapabilities.SupportsVisualStudioExtensions(); return new(markupKind, supportsVisualStudioExtensions); } diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/IRemoteHoverService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/IRemoteHoverService.cs new file mode 100644 index 0000000000..9a47dc792b --- /dev/null +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/IRemoteHoverService.cs @@ -0,0 +1,19 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.ExternalAccess.Razor; +using RoslynHover = Roslyn.LanguageServer.Protocol.Hover; +using RoslynPosition = Roslyn.LanguageServer.Protocol.Position; + +namespace Microsoft.CodeAnalysis.Razor.Remote; + +internal interface IRemoteHoverService : IRemoteJsonService +{ + ValueTask> GetHoverAsync( + JsonSerializableRazorPinnedSolutionInfoWrapper solutionInfo, + JsonSerializableDocumentId documentId, + RoslynPosition position, + CancellationToken cancellationToken); +} diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/RazorServices.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/RazorServices.cs index 5fcf2e65f3..d4888b7931 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/RazorServices.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/RazorServices.cs @@ -30,6 +30,7 @@ internal static class RazorServices [ (typeof(IRemoteClientInitializationService), null), (typeof(IRemoteGoToDefinitionService), null), + (typeof(IRemoteHoverService), null), (typeof(IRemoteSignatureHelpService), null), (typeof(IRemoteInlayHintService), null), (typeof(IRemoteDocumentSymbolService), null), diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Hover/RemoteHoverService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Hover/RemoteHoverService.cs new file mode 100644 index 0000000000..60a9807605 --- /dev/null +++ b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Hover/RemoteHoverService.cs @@ -0,0 +1,215 @@ +// 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; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Razor; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.CodeAnalysis.ExternalAccess.Razor; +using Microsoft.CodeAnalysis.Razor.DocumentMapping; +using Microsoft.CodeAnalysis.Razor.Hover; +using Microsoft.CodeAnalysis.Razor.Protocol; +using Microsoft.CodeAnalysis.Razor.Remote; +using Microsoft.CodeAnalysis.Remote.Razor.ProjectSystem; +using Roslyn.LanguageServer.Protocol; +using static Microsoft.CodeAnalysis.Razor.Remote.RemoteResponse; +using static Microsoft.VisualStudio.LanguageServer.Protocol.ClientCapabilitiesExtensions; +using static Microsoft.VisualStudio.LanguageServer.Protocol.VsLspExtensions; +using static Roslyn.LanguageServer.Protocol.RoslynLspExtensions; +using ExternalHandlers = Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost.Handlers; +using Range = Roslyn.LanguageServer.Protocol.Range; +using VsLsp = Microsoft.VisualStudio.LanguageServer.Protocol; + +namespace Microsoft.CodeAnalysis.Remote.Razor; + +internal sealed class RemoteHoverService(in ServiceArgs args) : RazorDocumentServiceBase(in args), IRemoteHoverService +{ + internal sealed class Factory : FactoryBase + { + protected override IRemoteHoverService CreateService(in ServiceArgs args) + => new RemoteHoverService(in args); + } + + private readonly IClientCapabilitiesService _clientCapabilitiesService = args.ExportProvider.GetExportedValue(); + + protected override IDocumentPositionInfoStrategy DocumentPositionInfoStrategy => PreferAttributeNameDocumentPositionInfoStrategy.Instance; + + public ValueTask> GetHoverAsync( + JsonSerializableRazorPinnedSolutionInfoWrapper solutionInfo, + JsonSerializableDocumentId documentId, + Position position, + CancellationToken cancellationToken) + => RunServiceAsync( + solutionInfo, + documentId, + context => GetHoverAsync(context, position, cancellationToken), + cancellationToken); + + private async ValueTask> GetHoverAsync( + RemoteDocumentContext context, + Position position, + CancellationToken cancellationToken) + { + var codeDocument = await context.GetCodeDocumentAsync(cancellationToken).ConfigureAwait(false); + + if (!codeDocument.Source.Text.TryGetAbsoluteIndex(position, out var hostDocumentIndex)) + { + return NoFurtherHandling; + } + + var clientCapabilities = _clientCapabilitiesService.ClientCapabilities; + var positionInfo = GetPositionInfo(codeDocument, hostDocumentIndex, preferCSharpOverHtml: true); + + if (positionInfo.LanguageKind == RazorLanguageKind.CSharp) + { + var generatedDocument = await context.Snapshot + .GetGeneratedDocumentAsync(cancellationToken) + .ConfigureAwait(false); + + var csharpHover = await ExternalHandlers.Hover + .GetHoverAsync( + generatedDocument, + positionInfo.Position.ToLinePosition(), + clientCapabilities.SupportsVisualStudioExtensions(), + clientCapabilities.SupportsMarkdown(), + cancellationToken) + .ConfigureAwait(false); + + // Roslyn couldn't provide a hover, so we're done. + if (csharpHover is null) + { + return NoFurtherHandling; + } + + // Map the hover range back to the host document + if (csharpHover.Range is { } range && + DocumentMappingService.TryMapToHostDocumentRange(codeDocument.GetCSharpDocument(), range.ToLinePositionSpan(), out var hostDocumentSpan)) + { + csharpHover.Range = RoslynLspFactory.CreateRange(hostDocumentSpan); + } + + return Results(csharpHover); + } + + if (positionInfo.LanguageKind is not (RazorLanguageKind.Html or RazorLanguageKind.Razor)) + { + Debug.Fail($"Encountered an unexpected {nameof(RazorLanguageKind)}: {positionInfo.LanguageKind}"); + return NoFurtherHandling; + } + + // If this is Html or Razor, try to retrieve a hover from Razor. + var options = HoverDisplayOptions.From(clientCapabilities); + + var razorHover = await HoverFactory + .GetHoverAsync(codeDocument, hostDocumentIndex, options, context.GetSolutionQueryOperations(), cancellationToken) + .ConfigureAwait(false); + + // Roslyn couldn't provide a hover, so we're done. + if (razorHover is null) + { + return CallHtml; + } + + // Ensure that we convert our Hover to a Roslyn Hover. + var resultHover = ConvertHover(razorHover); + + return Results(resultHover); + } + + /// + /// Converts a to a . + /// + /// + /// Once Razor moves wholly over to Roslyn.LanguageServer.Protocol, this method can be removed. + /// + private Hover ConvertHover(VsLsp.Hover hover) + { + // Note: Razor only ever produces a Hover with MarkupContent or a VSInternalHover with RawContents. + // Both variants return a Range. + + return hover switch + { + VsLsp.VSInternalHover { Range: var range, RawContent: { } rawContent } => new VSInternalHover() + { + Range = ConvertRange(range), + Contents = string.Empty, + RawContent = ConvertVsContent(rawContent) + }, + VsLsp.Hover { Range: var range, Contents.Fourth: VsLsp.MarkupContent contents } => new Hover() + { + Range = ConvertRange(range), + Contents = ConvertMarkupContent(contents) + }, + _ => Assumed.Unreachable(), + }; + + static Range? ConvertRange(VsLsp.Range? range) + { + return range is not null + ? RoslynLspFactory.CreateRange(range.ToLinePositionSpan()) + : null; + } + + static object ConvertVsContent(object obj) + { + return obj switch + { + VisualStudio.Core.Imaging.ImageId imageId => ConvertImageId(imageId), + VisualStudio.Text.Adornments.ImageElement element => ConvertImageElement(element), + VisualStudio.Text.Adornments.ClassifiedTextRun run => ConvertClassifiedTextRun(run), + VisualStudio.Text.Adornments.ClassifiedTextElement element => ConvertClassifiedTextElement(element), + VisualStudio.Text.Adornments.ContainerElement element => ConvertContainerElement(element), + _ => Assumed.Unreachable() + }; + + static Roslyn.Core.Imaging.ImageId ConvertImageId(VisualStudio.Core.Imaging.ImageId imageId) + { + return new(imageId.Guid, imageId.Id); + } + + static Roslyn.Text.Adornments.ImageElement ConvertImageElement(VisualStudio.Text.Adornments.ImageElement element) + { + return new(ConvertImageId(element.ImageId), element.AutomationName); + } + + static Roslyn.Text.Adornments.ClassifiedTextRun ConvertClassifiedTextRun(VisualStudio.Text.Adornments.ClassifiedTextRun run) + { + return new( + run.ClassificationTypeName, + run.Text, + (Roslyn.Text.Adornments.ClassifiedTextRunStyle)run.Style, + run.MarkerTagType, + run.NavigationAction, + run.Tooltip); + } + + static Roslyn.Text.Adornments.ClassifiedTextElement ConvertClassifiedTextElement(VisualStudio.Text.Adornments.ClassifiedTextElement element) + { + return new(element.Runs.Select(ConvertClassifiedTextRun)); + } + + static Roslyn.Text.Adornments.ContainerElement ConvertContainerElement(VisualStudio.Text.Adornments.ContainerElement element) + { + return new((Roslyn.Text.Adornments.ContainerElementStyle)element.Style, element.Elements.Select(ConvertVsContent)); + } + } + + static MarkupContent ConvertMarkupContent(VsLsp.MarkupContent value) + { + return new() + { + Kind = ConvertMarkupKind(value.Kind), + Value = value.Value + }; + } + + static MarkupKind ConvertMarkupKind(VsLsp.MarkupKind value) + { + return value == VsLsp.MarkupKind.Markdown + ? MarkupKind.Markdown + : MarkupKind.PlainText; + } + } +} diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostDocumentColorEndpoint.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostDocumentColorEndpoint.cs index c89eeae651..fe9e488157 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostDocumentColorEndpoint.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostDocumentColorEndpoint.cs @@ -32,7 +32,7 @@ internal class CohostDocumentColorEndpoint( protected override bool RequiresLSPSolution => true; - public ImmutableArray GetRegistrations(VSInternalClientCapabilities clientCapabilities, DocumentFilter[] filter, RazorCohostRequestContext requestContext) + public ImmutableArray GetRegistrations(VSInternalClientCapabilities clientCapabilities, RazorCohostRequestContext requestContext) { if (clientCapabilities.SupportsVisualStudioExtensions) { @@ -40,9 +40,6 @@ internal class CohostDocumentColorEndpoint( { Method = Methods.TextDocumentDocumentColorName, RegisterOptions = new DocumentColorRegistrationOptions() - { - DocumentSelector = filter - } }]; } diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostDocumentCompletionEndpoint.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostDocumentCompletionEndpoint.cs index a0b2ecb9cb..2783ba7b8d 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostDocumentCompletionEndpoint.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostDocumentCompletionEndpoint.cs @@ -55,7 +55,7 @@ internal sealed class CohostDocumentCompletionEndpoint( protected override bool RequiresLSPSolution => true; - public ImmutableArray GetRegistrations(VSInternalClientCapabilities clientCapabilities, DocumentFilter[] filter, RazorCohostRequestContext requestContext) + public ImmutableArray GetRegistrations(VSInternalClientCapabilities clientCapabilities, RazorCohostRequestContext requestContext) { if (clientCapabilities.TextDocument?.Completion?.DynamicRegistration is true) { @@ -66,7 +66,6 @@ internal sealed class CohostDocumentCompletionEndpoint( { ResolveProvider = false, // TODO - change to true when Resolve is implemented TriggerCharacters = CompletionTriggerAndCommitCharacters.AllTriggerCharacters, - DocumentSelector = filter, AllCommitCharacters = CompletionTriggerAndCommitCharacters.AllCommitCharacters } }]; @@ -188,7 +187,7 @@ internal sealed class CohostDocumentCompletionEndpoint( completionContext.TriggerCharacter); } - return combinedCompletionList; + return combinedCompletionList; } private async Task GetHtmlCompletionListAsync( @@ -219,7 +218,7 @@ internal sealed class CohostDocumentCompletionEndpoint( return rewrittenResponse; } - private static T? ToVsLSP(object source) where T : class + private static T? ToVsLSP(object source) where T : class { // This is, to say the least, not ideal. In future we're going to normalize on to Roslyn LSP types, and this can go. var options = new JsonSerializerOptions(); diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostDocumentFormattingEndpoint.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostDocumentFormattingEndpoint.cs index abe2019a97..6d37ea1f39 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostDocumentFormattingEndpoint.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostDocumentFormattingEndpoint.cs @@ -45,7 +45,7 @@ internal sealed class CohostDocumentFormattingEndpoint( protected override bool RequiresLSPSolution => true; - public ImmutableArray GetRegistrations(VSInternalClientCapabilities clientCapabilities, DocumentFilter[] filter, RazorCohostRequestContext requestContext) + public ImmutableArray GetRegistrations(VSInternalClientCapabilities clientCapabilities, RazorCohostRequestContext requestContext) { if (clientCapabilities.TextDocument?.Formatting?.DynamicRegistration is true) { @@ -53,9 +53,6 @@ internal sealed class CohostDocumentFormattingEndpoint( { Method = Methods.TextDocumentFormattingName, RegisterOptions = new DocumentFormattingRegistrationOptions() - { - DocumentSelector = filter - } }]; } diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostDocumentHighlightEndpoint.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostDocumentHighlightEndpoint.cs index 6159702a47..8a2115a534 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostDocumentHighlightEndpoint.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostDocumentHighlightEndpoint.cs @@ -38,7 +38,7 @@ internal class CohostDocumentHighlightEndpoint( protected override bool RequiresLSPSolution => true; - public ImmutableArray GetRegistrations(VSInternalClientCapabilities clientCapabilities, DocumentFilter[] filter, RazorCohostRequestContext requestContext) + public ImmutableArray GetRegistrations(VSInternalClientCapabilities clientCapabilities, RazorCohostRequestContext requestContext) { if (clientCapabilities.SupportsVisualStudioExtensions) { @@ -46,9 +46,6 @@ internal class CohostDocumentHighlightEndpoint( { Method = Methods.TextDocumentDocumentHighlightName, RegisterOptions = new DocumentHighlightRegistrationOptions() - { - DocumentSelector = filter - } }]; } diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostDocumentPullDiagnosticsEndpoint.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostDocumentPullDiagnosticsEndpoint.cs index e3194f25b4..4e06726449 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostDocumentPullDiagnosticsEndpoint.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostDocumentPullDiagnosticsEndpoint.cs @@ -50,7 +50,7 @@ internal class CohostDocumentPullDiagnosticsEndpoint( protected override bool RequiresLSPSolution => true; - public ImmutableArray GetRegistrations(VSInternalClientCapabilities clientCapabilities, DocumentFilter[] filter, RazorCohostRequestContext requestContext) + public ImmutableArray GetRegistrations(VSInternalClientCapabilities clientCapabilities, RazorCohostRequestContext requestContext) { // TODO: if (clientCapabilities.TextDocument?.Diagnostic?.DynamicRegistration is true) { @@ -59,7 +59,6 @@ internal class CohostDocumentPullDiagnosticsEndpoint( Method = VSInternalMethods.DocumentPullDiagnosticName, RegisterOptions = new VSInternalDiagnosticRegistrationOptions() { - DocumentSelector = filter, DiagnosticKinds = [VSInternalDiagnosticKind.Syntax] } }]; diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostDocumentSpellCheckEndpoint.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostDocumentSpellCheckEndpoint.cs index fed7d22954..f260cd2ef5 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostDocumentSpellCheckEndpoint.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostDocumentSpellCheckEndpoint.cs @@ -31,7 +31,7 @@ internal sealed class CohostDocumentSpellCheckEndpoint( protected override bool RequiresLSPSolution => true; - public ImmutableArray GetRegistrations(VSInternalClientCapabilities clientCapabilities, DocumentFilter[] filter, RazorCohostRequestContext requestContext) + public ImmutableArray GetRegistrations(VSInternalClientCapabilities clientCapabilities, RazorCohostRequestContext requestContext) { if (clientCapabilities.SupportsVisualStudioExtensions) { @@ -39,9 +39,6 @@ internal sealed class CohostDocumentSpellCheckEndpoint( { Method = VSInternalMethods.TextDocumentSpellCheckableRangesName, RegisterOptions = new TextDocumentRegistrationOptions() - { - DocumentSelector = filter - } }]; } diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostDocumentSymbolEndpoint.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostDocumentSymbolEndpoint.cs index 110260c22c..cade056caa 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostDocumentSymbolEndpoint.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostDocumentSymbolEndpoint.cs @@ -30,7 +30,7 @@ internal class CohostDocumentSymbolEndpoint(IRemoteServiceInvoker remoteServiceI protected override bool RequiresLSPSolution => true; - public ImmutableArray GetRegistrations(VSInternalClientCapabilities clientCapabilities, DocumentFilter[] filter, RazorCohostRequestContext requestContext) + public ImmutableArray GetRegistrations(VSInternalClientCapabilities clientCapabilities, RazorCohostRequestContext requestContext) { if (clientCapabilities.TextDocument?.DocumentSymbol?.DynamicRegistration == true) { @@ -40,9 +40,6 @@ internal class CohostDocumentSymbolEndpoint(IRemoteServiceInvoker remoteServiceI { Method = Methods.TextDocumentDocumentSymbolName, RegisterOptions = new DocumentSymbolRegistrationOptions() - { - DocumentSelector = filter - } }]; } diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostEndpointRegistration.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostEndpointRegistration.cs index 792b9fe85f..575e3cc83e 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostEndpointRegistration.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostEndpointRegistration.cs @@ -18,7 +18,7 @@ namespace Microsoft.VisualStudio.Razor.LanguageClient.Cohost; #pragma warning restore RS0030 // Do not use banned APIs internal sealed class CohostEndpointRegistration : IDynamicRegistrationProvider { - public ImmutableArray GetRegistrations(VSInternalClientCapabilities clientCapabilities, DocumentFilter[] filter, RazorCohostRequestContext requestContext) + public ImmutableArray GetRegistrations(VSInternalClientCapabilities clientCapabilities, RazorCohostRequestContext requestContext) { return [ // Project Context, for the nav bar @@ -26,25 +26,18 @@ internal sealed class CohostEndpointRegistration : IDynamicRegistrationProvider { Method = VSMethods.GetProjectContextsName, RegisterOptions = new TextDocumentRegistrationOptions() - { - DocumentSelector = filter - } }, // DidOpen, DidChange, DidClose, for document synchronization new Registration { Method = Methods.TextDocumentDidOpenName, RegisterOptions = new TextDocumentRegistrationOptions() - { - DocumentSelector = filter - } }, new Registration { Method = Methods.TextDocumentDidChangeName, RegisterOptions = new TextDocumentChangeRegistrationOptions() { - DocumentSelector = filter, SyncKind = TextDocumentSyncKind.Incremental } }, @@ -52,9 +45,6 @@ internal sealed class CohostEndpointRegistration : IDynamicRegistrationProvider { Method = Methods.TextDocumentDidCloseName, RegisterOptions = new TextDocumentRegistrationOptions() - { - DocumentSelector = filter - } }, ]; } diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostFoldingRangeEndpoint.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostFoldingRangeEndpoint.cs index 6976912249..5313557ec1 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostFoldingRangeEndpoint.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostFoldingRangeEndpoint.cs @@ -41,7 +41,7 @@ internal class CohostFoldingRangeEndpoint( protected override bool RequiresLSPSolution => true; - public ImmutableArray GetRegistrations(VSInternalClientCapabilities clientCapabilities, DocumentFilter[] filter, RazorCohostRequestContext requestContext) + public ImmutableArray GetRegistrations(VSInternalClientCapabilities clientCapabilities, RazorCohostRequestContext requestContext) { if (clientCapabilities.TextDocument?.FoldingRange?.DynamicRegistration is true) { @@ -49,9 +49,6 @@ internal class CohostFoldingRangeEndpoint( { Method = Methods.TextDocumentFoldingRangeName, RegisterOptions = new FoldingRangeRegistrationOptions() - { - DocumentSelector = filter - } }]; } diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostGoToDefinitionEndpoint.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostGoToDefinitionEndpoint.cs index 2fa3632525..73f89001b8 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostGoToDefinitionEndpoint.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostGoToDefinitionEndpoint.cs @@ -45,14 +45,14 @@ internal sealed class CohostGoToDefinitionEndpoint( protected override bool RequiresLSPSolution => true; - public ImmutableArray GetRegistrations(VSInternalClientCapabilities clientCapabilities, DocumentFilter[] filter, RazorCohostRequestContext requestContext) + public ImmutableArray GetRegistrations(VSInternalClientCapabilities clientCapabilities, RazorCohostRequestContext requestContext) { if (clientCapabilities.TextDocument?.Definition?.DynamicRegistration == true) { return [new Registration { Method = Methods.TextDocumentDefinitionName, - RegisterOptions = new DefinitionOptions() + RegisterOptions = new DefinitionRegistrationOptions() }]; } diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostGoToImplementationEndpoint.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostGoToImplementationEndpoint.cs index 4e4f529c2c..9c44c22785 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostGoToImplementationEndpoint.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostGoToImplementationEndpoint.cs @@ -41,14 +41,14 @@ internal sealed class CohostGoToImplementationEndpoint( protected override bool RequiresLSPSolution => true; - public ImmutableArray GetRegistrations(VSInternalClientCapabilities clientCapabilities, DocumentFilter[] filter, RazorCohostRequestContext requestContext) + public ImmutableArray GetRegistrations(VSInternalClientCapabilities clientCapabilities, RazorCohostRequestContext requestContext) { if (clientCapabilities.TextDocument?.Implementation?.DynamicRegistration == true) { return [new Registration { Method = Methods.TextDocumentImplementationName, - RegisterOptions = new ImplementationOptions() + RegisterOptions = new ImplementationRegistrationOptions() }]; } diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostHoverEndpoint.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostHoverEndpoint.cs new file mode 100644 index 0000000000..ceac27d04c --- /dev/null +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostHoverEndpoint.cs @@ -0,0 +1,119 @@ +// 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.Immutable; +using System.Composition; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Razor; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost; +using Microsoft.CodeAnalysis.Razor.Remote; +using Microsoft.VisualStudio.LanguageServer.ContainedLanguage; +using Microsoft.VisualStudio.LanguageServer.Protocol; +using RoslynHover = Roslyn.LanguageServer.Protocol.Hover; +using RoslynLspFactory = Roslyn.LanguageServer.Protocol.RoslynLspFactory; +using VsHover = Microsoft.VisualStudio.LanguageServer.Protocol.Hover; + +namespace Microsoft.VisualStudio.Razor.LanguageClient.Cohost; + +#pragma warning disable RS0030 // Do not use banned APIs +[Shared] +[CohostEndpoint(Methods.TextDocumentHoverName)] +[Export(typeof(IDynamicRegistrationProvider))] +[ExportCohostStatelessLspService(typeof(CohostHoverEndpoint))] +[method: ImportingConstructor] +#pragma warning restore RS0030 // Do not use banned APIs +internal sealed class CohostHoverEndpoint( + IRemoteServiceInvoker remoteServiceInvoker, + IHtmlDocumentSynchronizer htmlDocumentSynchronizer, + LSPRequestInvoker requestInvoker) + : AbstractRazorCohostDocumentRequestHandler?>, IDynamicRegistrationProvider +{ + private readonly IRemoteServiceInvoker _remoteServiceInvoker = remoteServiceInvoker; + private readonly IHtmlDocumentSynchronizer _htmlDocumentSynchronizer = htmlDocumentSynchronizer; + private readonly LSPRequestInvoker _requestInvoker = requestInvoker; + + protected override bool MutatesSolutionState => false; + + protected override bool RequiresLSPSolution => true; + + public ImmutableArray GetRegistrations(VSInternalClientCapabilities clientCapabilities, RazorCohostRequestContext requestContext) + { + if (clientCapabilities.TextDocument?.Hover?.DynamicRegistration == true) + { + return [new Registration + { + Method = Methods.TextDocumentHoverName, + RegisterOptions = new HoverRegistrationOptions() + }]; + } + + return []; + } + + protected override RazorTextDocumentIdentifier? GetRazorTextDocumentIdentifier(TextDocumentPositionParams request) + => request.TextDocument.ToRazorTextDocumentIdentifier(); + + protected override Task?> HandleRequestAsync(TextDocumentPositionParams request, RazorCohostRequestContext context, CancellationToken cancellationToken) + => HandleRequestAsync( + request, + context.TextDocument.AssumeNotNull(), + cancellationToken); + + private async Task?> HandleRequestAsync(TextDocumentPositionParams request, TextDocument razorDocument, CancellationToken cancellationToken) + { + var position = RoslynLspFactory.CreatePosition(request.Position.ToLinePosition()); + + var response = await _remoteServiceInvoker + .TryInvokeAsync>( + razorDocument.Project.Solution, + (service, solutionInfo, cancellationToken) => + service.GetHoverAsync(solutionInfo, razorDocument.Id, position, cancellationToken), + cancellationToken) + .ConfigureAwait(false); + + if (response.Result is RoslynHover hover) + { + return hover; + } + + if (response.StopHandling) + { + return null; + } + + return await GetHtmlHoverAsync(request, razorDocument, cancellationToken).ConfigureAwait(false); + } + + private async Task GetHtmlHoverAsync(TextDocumentPositionParams request, TextDocument razorDocument, CancellationToken cancellationToken) + { + var htmlDocument = await _htmlDocumentSynchronizer.TryGetSynchronizedHtmlDocumentAsync(razorDocument, cancellationToken).ConfigureAwait(false); + if (htmlDocument is null) + { + return null; + } + + request.TextDocument = request.TextDocument.WithUri(htmlDocument.Uri); + + var result = await _requestInvoker + .ReinvokeRequestOnServerAsync( + htmlDocument.Buffer, + Methods.TextDocumentHoverName, + RazorLSPConstants.HtmlLanguageServerName, + request, + cancellationToken) + .ConfigureAwait(false); + + return result?.Response; + } + + internal TestAccessor GetTestAccessor() => new(this); + + internal readonly struct TestAccessor(CohostHoverEndpoint instance) + { + public Task?> HandleRequestAsync( + TextDocumentPositionParams request, TextDocument razorDocument, CancellationToken cancellationToken) + => instance.HandleRequestAsync(request, razorDocument, cancellationToken); + } +} diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostInlayHintEndpoint.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostInlayHintEndpoint.cs index bf7c98f03f..ad6b4941ba 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostInlayHintEndpoint.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostInlayHintEndpoint.cs @@ -30,7 +30,7 @@ internal class CohostInlayHintEndpoint(IRemoteServiceInvoker remoteServiceInvoke protected override bool RequiresLSPSolution => true; - public ImmutableArray GetRegistrations(VSLSP.VSInternalClientCapabilities clientCapabilities, VSLSP.DocumentFilter[] filter, RazorCohostRequestContext requestContext) + public ImmutableArray GetRegistrations(VSLSP.VSInternalClientCapabilities clientCapabilities, RazorCohostRequestContext requestContext) { if (clientCapabilities.TextDocument?.InlayHint?.DynamicRegistration == true) { @@ -38,9 +38,6 @@ internal class CohostInlayHintEndpoint(IRemoteServiceInvoker remoteServiceInvoke { Method = Methods.TextDocumentInlayHintName, RegisterOptions = new VSLSP.InlayHintRegistrationOptions() - { - DocumentSelector = filter - } }]; } diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostLinkedEditingRangeEndpoint.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostLinkedEditingRangeEndpoint.cs index 23a08440fc..a378989444 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostLinkedEditingRangeEndpoint.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostLinkedEditingRangeEndpoint.cs @@ -32,7 +32,7 @@ internal class CohostLinkedEditingRangeEndpoint(IRemoteServiceInvoker remoteServ protected override bool RequiresLSPSolution => true; - public ImmutableArray GetRegistrations(VSInternalClientCapabilities clientCapabilities, DocumentFilter[] filter, RazorCohostRequestContext requestContext) + public ImmutableArray GetRegistrations(VSInternalClientCapabilities clientCapabilities, RazorCohostRequestContext requestContext) { if (clientCapabilities.TextDocument?.LinkedEditingRange?.DynamicRegistration == true) { @@ -40,9 +40,6 @@ internal class CohostLinkedEditingRangeEndpoint(IRemoteServiceInvoker remoteServ { Method = Methods.TextDocumentLinkedEditingRangeName, RegisterOptions = new LinkedEditingRangeRegistrationOptions() - { - DocumentSelector = filter - } }]; } diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostOnAutoInsertEndpoint.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostOnAutoInsertEndpoint.cs index b8c9fd42b1..dc62194024 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostOnAutoInsertEndpoint.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostOnAutoInsertEndpoint.cs @@ -67,14 +67,14 @@ internal class CohostOnAutoInsertEndpoint( protected override bool RequiresLSPSolution => true; - public ImmutableArray GetRegistrations(VSInternalClientCapabilities clientCapabilities, DocumentFilter[] filter, RazorCohostRequestContext requestContext) + public ImmutableArray GetRegistrations(VSInternalClientCapabilities clientCapabilities, RazorCohostRequestContext requestContext) { if (clientCapabilities.SupportsVisualStudioExtensions) { return [new Registration { Method = VSInternalMethods.OnAutoInsertName, - RegisterOptions = new VSInternalDocumentOnAutoInsertOptions() + RegisterOptions = new VSInternalDocumentOnAutoInsertRegistrationOptions() .EnableOnAutoInsert(_triggerCharacters) }]; } diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostOnTypeFormattingEndpoint.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostOnTypeFormattingEndpoint.cs index 636da1a73b..f189d74faf 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostOnTypeFormattingEndpoint.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostOnTypeFormattingEndpoint.cs @@ -46,7 +46,7 @@ internal sealed class CohostOnTypeFormattingEndpoint( protected override bool RequiresLSPSolution => true; - public ImmutableArray GetRegistrations(VSInternalClientCapabilities clientCapabilities, DocumentFilter[] filter, RazorCohostRequestContext requestContext) + public ImmutableArray GetRegistrations(VSInternalClientCapabilities clientCapabilities, RazorCohostRequestContext requestContext) { if (clientCapabilities.TextDocument?.Formatting?.DynamicRegistration is true) { @@ -54,9 +54,7 @@ internal sealed class CohostOnTypeFormattingEndpoint( { Method = Methods.TextDocumentOnTypeFormattingName, RegisterOptions = new DocumentOnTypeFormattingRegistrationOptions() - { - DocumentSelector = filter, - }.EnableOnTypeFormattingTriggerCharacters() + .EnableOnTypeFormattingTriggerCharacters() }]; } diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostRangeFormattingEndpoint.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostRangeFormattingEndpoint.cs index a849a6d05c..f1fbbd60f7 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostRangeFormattingEndpoint.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostRangeFormattingEndpoint.cs @@ -45,7 +45,7 @@ internal sealed class CohostRangeFormattingEndpoint( protected override bool RequiresLSPSolution => true; - public ImmutableArray GetRegistrations(VSInternalClientCapabilities clientCapabilities, DocumentFilter[] filter, RazorCohostRequestContext requestContext) + public ImmutableArray GetRegistrations(VSInternalClientCapabilities clientCapabilities, RazorCohostRequestContext requestContext) { if (clientCapabilities.TextDocument?.Formatting?.DynamicRegistration is true) { @@ -53,9 +53,6 @@ internal sealed class CohostRangeFormattingEndpoint( { Method = Methods.TextDocumentRangeFormattingName, RegisterOptions = new DocumentRangeFormattingRegistrationOptions() - { - DocumentSelector = filter - } }]; } diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostRenameEndpoint.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostRenameEndpoint.cs index e0877c3446..12e0c947cc 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostRenameEndpoint.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostRenameEndpoint.cs @@ -35,7 +35,7 @@ internal class CohostRenameEndpoint( protected override bool RequiresLSPSolution => true; - public ImmutableArray GetRegistrations(VSInternalClientCapabilities clientCapabilities, DocumentFilter[] filter, RazorCohostRequestContext requestContext) + public ImmutableArray GetRegistrations(VSInternalClientCapabilities clientCapabilities, RazorCohostRequestContext requestContext) { if (clientCapabilities.TextDocument?.Rename?.DynamicRegistration == true) { @@ -44,7 +44,6 @@ internal class CohostRenameEndpoint( Method = Methods.TextDocumentRenameName, RegisterOptions = new RenameRegistrationOptions() { - DocumentSelector = filter, PrepareProvider = false } }]; diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostSemanticTokensRangeEndpoint.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostSemanticTokensRangeEndpoint.cs index a2fe70bd47..29f89c3881 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostSemanticTokensRangeEndpoint.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostSemanticTokensRangeEndpoint.cs @@ -43,7 +43,7 @@ internal sealed class CohostSemanticTokensRangeEndpoint( protected override bool MutatesSolutionState => false; protected override bool RequiresLSPSolution => true; - public ImmutableArray GetRegistrations(VSInternalClientCapabilities clientCapabilities, DocumentFilter[] filter, RazorCohostRequestContext requestContext) + public ImmutableArray GetRegistrations(VSInternalClientCapabilities clientCapabilities, RazorCohostRequestContext requestContext) { if (clientCapabilities.TextDocument?.SemanticTokens?.DynamicRegistration == true) { @@ -55,9 +55,7 @@ internal sealed class CohostSemanticTokensRangeEndpoint( { Method = Methods.TextDocumentSemanticTokensRangeName, RegisterOptions = new SemanticTokensRegistrationOptions() - { - DocumentSelector = filter, - }.EnableSemanticTokens(_semanticTokensLegendService) + .EnableSemanticTokens(_semanticTokensLegendService) }]; } diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostSignatureHelpEndpoint.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostSignatureHelpEndpoint.cs index 39927d7465..37506521e1 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostSignatureHelpEndpoint.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostSignatureHelpEndpoint.cs @@ -46,7 +46,7 @@ internal class CohostSignatureHelpEndpoint( protected override bool RequiresLSPSolution => true; - public ImmutableArray GetRegistrations(VSInternalClientCapabilities clientCapabilities, DocumentFilter[] filter, RazorCohostRequestContext requestContext) + public ImmutableArray GetRegistrations(VSInternalClientCapabilities clientCapabilities, RazorCohostRequestContext requestContext) { if (clientCapabilities.TextDocument?.SignatureHelp?.DynamicRegistration == true) { @@ -54,9 +54,7 @@ internal class CohostSignatureHelpEndpoint( { Method = Methods.TextDocumentSignatureHelpName, RegisterOptions = new SignatureHelpRegistrationOptions() - { - DocumentSelector = filter - }.EnableSignatureHelp() + .EnableSignatureHelp() }]; } diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostTextPresentationEndpoint.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostTextPresentationEndpoint.cs index 792b09ad6e..5686cdcc04 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostTextPresentationEndpoint.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostTextPresentationEndpoint.cs @@ -35,7 +35,7 @@ internal class CohostTextPresentationEndpoint( protected override bool RequiresLSPSolution => true; - public ImmutableArray GetRegistrations(VSInternalClientCapabilities clientCapabilities, DocumentFilter[] filter, RazorCohostRequestContext requestContext) + public ImmutableArray GetRegistrations(VSInternalClientCapabilities clientCapabilities, RazorCohostRequestContext requestContext) { if (clientCapabilities.SupportsVisualStudioExtensions) { @@ -43,9 +43,6 @@ internal class CohostTextPresentationEndpoint( { Method = VSInternalMethods.TextDocumentTextPresentationName, RegisterOptions = new TextDocumentRegistrationOptions() - { - DocumentSelector = filter - } }]; } diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostUriPresentationEndpoint.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostUriPresentationEndpoint.cs index b74b73e750..005cd6db9a 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostUriPresentationEndpoint.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostUriPresentationEndpoint.cs @@ -39,7 +39,7 @@ internal class CohostUriPresentationEndpoint( protected override bool RequiresLSPSolution => true; - public ImmutableArray GetRegistrations(VSInternalClientCapabilities clientCapabilities, DocumentFilter[] filter, RazorCohostRequestContext requestContext) + public ImmutableArray GetRegistrations(VSInternalClientCapabilities clientCapabilities, RazorCohostRequestContext requestContext) { if (clientCapabilities.SupportsVisualStudioExtensions) { @@ -47,9 +47,6 @@ internal class CohostUriPresentationEndpoint( { Method = VSInternalMethods.TextDocumentUriPresentationName, RegisterOptions = new TextDocumentRegistrationOptions() - { - DocumentSelector = filter - } }]; } diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/IDynamicRegistrationProvider.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/IDynamicRegistrationProvider.cs index 84d2e23333..c76e616460 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/IDynamicRegistrationProvider.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/IDynamicRegistrationProvider.cs @@ -9,5 +9,5 @@ namespace Microsoft.VisualStudio.Razor.LanguageClient.Cohost; internal interface IDynamicRegistrationProvider { - ImmutableArray GetRegistrations(VSInternalClientCapabilities clientCapabilities, DocumentFilter[] filter, RazorCohostRequestContext requestContext); + ImmutableArray GetRegistrations(VSInternalClientCapabilities clientCapabilities, RazorCohostRequestContext requestContext); } diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/RazorCohostDynamicRegistrationService.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/RazorCohostDynamicRegistrationService.cs index 20471f0657..4d94bc5a36 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/RazorCohostDynamicRegistrationService.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/RazorCohostDynamicRegistrationService.cs @@ -23,7 +23,7 @@ internal class RazorCohostDynamicRegistrationService( Lazy lazyRazorCohostClientCapabilitiesService) : IRazorCohostDynamicRegistrationService { - private readonly DocumentFilter[] _filter = [new DocumentFilter() + private static readonly DocumentFilter[] s_filter = [new DocumentFilter() { Language = Constants.RazorLanguageName, Pattern = "**/*.{razor,cshtml}" @@ -50,10 +50,15 @@ internal class RazorCohostDynamicRegistrationService( foreach (var provider in _lazyRegistrationProviders) { - foreach (var registration in provider.Value.GetRegistrations(clientCapabilities, _filter, requestContext)) + foreach (var registration in provider.Value.GetRegistrations(clientCapabilities, requestContext)) { // We don't unregister anything, so we don't need to do anything interesting with Ids registration.Id = Guid.NewGuid().ToString(); + if (registration.RegisterOptions is ITextDocumentRegistrationOptions options) + { + options.DocumentSelector = s_filter; + } + registrations.Add(registration); } } diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostEndpointTest.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostEndpointTest.cs new file mode 100644 index 0000000000..f25bf85c9c --- /dev/null +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostEndpointTest.cs @@ -0,0 +1,186 @@ +// 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.Collections.Generic; +using System.ComponentModel.Composition; +using System.Linq; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Razor.Test.Common; +using Microsoft.AspNetCore.Razor.Test.Common.Mef; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost; +using Microsoft.CodeAnalysis.Razor.Workspaces; +using Microsoft.VisualStudio.LanguageServer.Client; +using Microsoft.VisualStudio.LanguageServer.ContainedLanguage; +using Microsoft.VisualStudio.LanguageServer.Protocol; +using Microsoft.VisualStudio.Razor; +using Microsoft.VisualStudio.Razor.LanguageClient.Cohost; +using Microsoft.VisualStudio.Text; +using Microsoft.VisualStudio.Threading; +using Newtonsoft.Json.Linq; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.VisualStudio.LanguageServices.Razor.Test.Cohost; + +public class CohostEndpointTest(ITestOutputHelper testOutputHelper) : ToolingTestBase(testOutputHelper) +{ + [Fact] + public void EndpointsHaveUniqueLSPMethods() + { + var methods = new Dictionary(); + + var endpoints = typeof(CohostColorPresentationEndpoint).Assembly.GetTypes() + .Where(t => t.GetCustomAttribute() != null) + .Select(t => (t, t.GetCustomAttribute().Method)); + + foreach (var (endpointType, method) in endpoints) + { + if (methods.TryGetValue(method, out var existing)) + { + Assert.Fail($"Could not register {endpointType.Name} for {method} because {existing.Name} already has."); + } + + methods.Add(method, endpointType); + } + } + + [Fact] + public void RegistrationsProvideFilter() + { + var testComposition = TestComposition.Roslyn + .Add(TestComposition.Editor) + .AddAssemblies(typeof(CohostLinkedEditingRangeEndpoint).Assembly) + .AddAssemblies(typeof(DefaultLSPRequestInvoker).Assembly) + .AddAssemblies(typeof(LanguageServerFeatureOptions).Assembly) + .AddParts(typeof(TestILanguageServiceBroker2)) + .AddExcludedPartTypes(typeof(IWorkspaceProvider)) + .AddParts(typeof(TestWorkspaceProvider)) + .AddExcludedPartTypes(typeof(ILspEditorFeatureDetector)) + .AddParts(typeof(TestLspEditorFeatureDetector)); + + using var exportProvider = testComposition.ExportProviderFactory.CreateExportProvider(); + + var providers = exportProvider.GetExportedValues().ToList(); + + // First we verify that the MEF composition above is correct, otherwise this test will be invalid + var actualProviders = typeof(CohostLinkedEditingRangeEndpoint).Assembly.GetTypes().Where(t => !t.IsInterface && typeof(IDynamicRegistrationProvider).IsAssignableFrom(t)).ToList(); + Assert.Equal(actualProviders.OrderBy(a => a.Name).Select(r => r.Name).ToArray(), providers.OrderBy(e => e.GetType().Name).Select(r => r.GetType().Name).ToArray()); + + var clientCapabilities = new VSInternalClientCapabilities() + { + SupportsVisualStudioExtensions = true, + TextDocument = new TextDocumentClientCapabilities() + { + CodeAction = new() { DynamicRegistration = true }, + CodeLens = new() { DynamicRegistration = true }, + Completion = new() { DynamicRegistration = true }, + Definition = new() { DynamicRegistration = true }, + Diagnostic = new() { DynamicRegistration = true }, + DocumentHighlight = new() { DynamicRegistration = true }, + DocumentLink = new() { DynamicRegistration = true }, + DocumentSymbol = new() { DynamicRegistration = true }, + FoldingRange = new() { DynamicRegistration = true }, + Formatting = new() { DynamicRegistration = true }, + Hover = new() { DynamicRegistration = true }, + Implementation = new() { DynamicRegistration = true }, + InlayHint = new() { DynamicRegistration = true }, + LinkedEditingRange = new() { DynamicRegistration = true }, + OnTypeFormatting = new() { DynamicRegistration = true }, + RangeFormatting = new() { DynamicRegistration = true }, + References = new() { DynamicRegistration = true }, + Rename = new() { DynamicRegistration = true }, + SemanticTokens = new() { DynamicRegistration = true }, + SignatureHelp = new() { DynamicRegistration = true }, + Synchronization = new() { DynamicRegistration = true }, + TypeDefinition = new() { DynamicRegistration = true } + }, + }; + + foreach (var endpoint in providers) + { + if (endpoint is CohostSemanticTokensRangeEndpoint) + { + // We can't currently test this, as the GetRegistrations method calls requestContext.GetRequiredService + // and we can't create a request context ourselves + continue; + } + + var registrations = endpoint.GetRegistrations(clientCapabilities, requestContext: new()); + + // If we didn't get any registrations then the test is probably invalid, and we need to update client capabilities above + if (registrations.Length == 0) + { + Assert.Fail($"Did not get any registrations from {endpoint.GetType().Name}. Client capabilities might be wrong?"); + } + + foreach (var registration in registrations) + { + var options = registration.RegisterOptions as ITextDocumentRegistrationOptions; + if (options is null) + { + Assert.Fail($"Could not convert registration options from {endpoint.GetType().Name} to {nameof(ITextDocumentRegistrationOptions)}. It was {registration.RegisterOptions?.GetType().Name}"); + } + } + } + } + + [Export(typeof(ILanguageServiceBroker2))] + private class TestILanguageServiceBroker2 : ILanguageServiceBroker2 + { + public IEnumerable ActiveLanguageClients => throw new NotImplementedException(); + public IEnumerable> FactoryLanguageClients => throw new NotImplementedException(); + public IEnumerable> LanguageClients => throw new NotImplementedException(); + + public event EventHandler LanguageClientLoaded { add { } remove { } } + public event AsyncEventHandler ClientNotifyAsync { add { } remove { } } + + public void AddCustomBufferContentTypes(IEnumerable contentTypes) => throw new NotImplementedException(); + public void AddLanguageClients(IEnumerable> items) => throw new NotImplementedException(); + public Task LoadAsync(IContentTypeMetadata contentType, ILanguageClient client) => throw new NotImplementedException(); + public void Notify(Notification notification, CancellationToken cancellationToken) => throw new NotImplementedException(); + public Task NotifyAsync(ILanguageClient languageClient, string method, JToken parameters, CancellationToken cancellationToken) => throw new NotImplementedException(); + public Task OnDidChangeTextDocumentAsync(ITextSnapshot before, ITextSnapshot after, IEnumerable textChanges) => throw new NotImplementedException(); + public Task OnDidCloseTextDocumentAsync(ITextSnapshot snapShot) => throw new NotImplementedException(); + public Task OnDidOpenTextDocumentAsync(ITextSnapshot snapShot) => throw new NotImplementedException(); + public Task OnDidSaveTextDocumentAsync(ITextDocument document) => throw new NotImplementedException(); + public void RemoveCustomBufferContentTypes(IEnumerable contentTypes) => throw new NotImplementedException(); + public void RemoveLanguageClients(IEnumerable> items) => throw new NotImplementedException(); + public IAsyncEnumerable<(string client, TResponse? response)> RequestAllAsync(Request request, CancellationToken cancellationToken) => throw new NotImplementedException(); + public Task<(ILanguageClient?, JToken?)> RequestAsync(string[] contentTypes, Func capabilitiesFilter, string method, JToken parameters, CancellationToken cancellationToken) => throw new NotImplementedException(); + public Task<(ILanguageClient?, JToken?)> RequestAsync(string[] contentTypes, Func capabilitiesFilter, string clientName, string method, JToken parameters, CancellationToken cancellationToken) => throw new NotImplementedException(); + public Task RequestAsync(ITextBuffer textBuffer, Func capabilitiesFilter, string languageServerName, string method, Func parameterFactory, CancellationToken cancellationToken) => throw new NotImplementedException(); + public Task RequestAsync(ILanguageClient languageClient, string method, JToken parameters, CancellationToken cancellationToken) => throw new NotImplementedException(); + public Task RequestAsync(Request request, CancellationToken cancellationToken) => throw new NotImplementedException(); + public Task<(ILanguageClient?, TOut)> RequestAsync(string[] contentTypes, Func capabilitiesFilter, LspRequest method, TIn parameters, CancellationToken cancellationToken) => throw new NotImplementedException(); + public Task RequestAsync(ILanguageClient languageClient, LspRequest method, TIn parameters, CancellationToken cancellationToken) => throw new NotImplementedException(); + public Task> RequestMultipleAsync(string[] contentTypes, Func capabilitiesFilter, string method, JToken parameters, CancellationToken cancellationToken) => throw new NotImplementedException(); + public IAsyncEnumerable RequestMultipleAsync(ITextBuffer textBuffer, Func capabilitiesFilter, string method, Func parameterFactory, CancellationToken cancellationToken) => throw new NotImplementedException(); + public Task> RequestMultipleAsync(string[] contentTypes, Func capabilitiesFilter, LspRequest method, TIn parameters, CancellationToken cancellationToken) => throw new NotImplementedException(); + } + + [Export(typeof(IWorkspaceProvider))] + private class TestWorkspaceProvider : IWorkspaceProvider + { + public CodeAnalysis.Workspace GetWorkspace() => throw new NotImplementedException(); + } + + [Export(typeof(ILspEditorFeatureDetector))] + private class TestLspEditorFeatureDetector : ILspEditorFeatureDetector + { + public bool IsLiveShareHost() => throw new NotImplementedException(); + public bool IsLspEditorEnabled() => throw new NotImplementedException(); + public bool IsLspEditorSupported(string documentFilePath) => throw new NotImplementedException(); + public bool IsRemoteClient() => throw new NotImplementedException(); + } + + [Export(typeof(IRazorSemanticTokensRefreshQueue))] + private class TestRazorSemanticTokensRefreshQueue : IRazorSemanticTokensRefreshQueue + { + public void Initialize(string clientCapabilitiesString) => throw new NotImplementedException(); + public Task TryEnqueueRefreshComputationAsync(Project project, CancellationToken cancellationToken) => throw new NotImplementedException(); + } +} diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostHoverEndpointTest.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostHoverEndpointTest.cs new file mode 100644 index 0000000000..b8b40c37c2 --- /dev/null +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostHoverEndpointTest.cs @@ -0,0 +1,148 @@ +// 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.Threading.Tasks; +using Microsoft.AspNetCore.Razor.Test.Common; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.ExternalAccess.Razor; +using Microsoft.CodeAnalysis.Text; +using Microsoft.VisualStudio.LanguageServer.Protocol; +using Xunit; +using Xunit.Abstractions; +using RoslynHover = Roslyn.LanguageServer.Protocol.Hover; + +namespace Microsoft.VisualStudio.Razor.LanguageClient.Cohost; + +using static HoverAssertions; + +public class CohostHoverEndpointTest(ITestOutputHelper testOutputHelper) : CohostEndpointTestBase(testOutputHelper) +{ + [Fact] + public async Task Razor() + { + TestCode code = """ + <[|PageTi$$tle|]> +
+ + @{ + var myVariable = "Hello"; + + var length = myVariable.Length; + } + """; + + await VerifyHoverAsync(code, async (hover, document) => + { + await hover.VerifyRangeAsync(code.Span, document); + + hover.VerifyRawContent( + Container( + Container( + Image, + ClassifiedText( + Text("Microsoft"), + Punctuation("."), + Text("AspNetCore"), + Punctuation("."), + Text("Components"), + Punctuation("."), + Text("Web"), + Punctuation("."), + Type("PageTitle"))))); + }); + } + + [Fact] + public async Task Html() + { + TestCode code = """ + + + + @{ + var myVariable = "Hello"; + + var length = myVariable.Length; + } + """; + + var htmlResponse = new VSInternalHover(); + + await VerifyHoverAsync(code, htmlResponse, h => Assert.Same(htmlResponse, h)); + } + + [Fact] + public async Task CSharp() + { + TestCode code = """ + +
+ + @{ + var $$[|myVariable|] = "Hello"; + + var length = myVariable.Length; + } + """; + + await VerifyHoverAsync(code, async (hover, document) => + { + await hover.VerifyRangeAsync(code.Span, document); + + hover.VerifyRawContent( + Container( + Container( + Image, + ClassifiedText( + Punctuation("("), + Text("local variable"), + Punctuation(")"), + WhiteSpace(" "), + Keyword("string"), + WhiteSpace(" "), + LocalName("myVariable"))))); + }); + } + + private async Task VerifyHoverAsync(TestCode input, Func verifyHover) + { + var document = await CreateProjectAndRazorDocumentAsync(input.Text); + var result = await GetHoverResultAsync(document, input); + + Assert.NotNull(result); + var value = result.GetValueOrDefault(); + + Assert.True(value.TryGetFirst(out var hover)); + await verifyHover(hover, document); + } + + private async Task VerifyHoverAsync(TestCode input, Hover htmlResponse, Action verifyHover) + { + var document = await CreateProjectAndRazorDocumentAsync(input.Text); + var result = await GetHoverResultAsync(document, input, htmlResponse); + + Assert.NotNull(result); + var value = result.GetValueOrDefault(); + + Assert.True(value.TryGetSecond(out var hover)); + verifyHover(hover); + } + + private async Task?> GetHoverResultAsync(TextDocument document, TestCode input, Hover? htmlResponse = null) + { + var inputText = await document.GetTextAsync(DisposalToken); + var linePosition = inputText.GetLinePosition(input.Position); + + var requestInvoker = new TestLSPRequestInvoker([(Methods.TextDocumentHoverName, htmlResponse)]); + var endpoint = new CohostHoverEndpoint(RemoteServiceInvoker, TestHtmlDocumentSynchronizer.Instance, requestInvoker); + + var textDocumentPositionParams = new TextDocumentPositionParams + { + Position = VsLspFactory.CreatePosition(linePosition), + TextDocument = new TextDocumentIdentifier { Uri = document.CreateUri() }, + }; + + return await endpoint.GetTestAccessor().HandleRequestAsync(textDocumentPositionParams, document, DisposalToken); + } +} diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/HoverAssertions.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/HoverAssertions.cs new file mode 100644 index 0000000000..59351ed6c2 --- /dev/null +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/HoverAssertions.cs @@ -0,0 +1,99 @@ +// 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.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Classification; +using Microsoft.CodeAnalysis.Razor.Tooltip; +using Microsoft.CodeAnalysis.Text; +using Roslyn.LanguageServer.Protocol; +using Roslyn.Text.Adornments; +using Xunit; + +namespace Microsoft.VisualStudio.Razor.LanguageClient.Cohost; + +internal static class HoverAssertions +{ + public static async Task VerifyRangeAsync(this Hover hover, TextSpan expected, TextDocument document) + { + var text = await document.GetTextAsync(); + Assert.NotNull(hover.Range); + Assert.Equal(text.GetLinePositionSpan(expected), hover.Range.ToLinePositionSpan()); + } + + public static void VerifyRawContent(this Hover hover, Action verifier) + { + var vsHover = Assert.IsType(hover); + verifier(vsHover.RawContent); + } + + public static Action Container(params ImmutableArray> elements) + => o => + { + Assert.NotNull(o); + var container = Assert.IsType(o); + + var allElements = container.Elements.ToArray(); + Assert.Equal(elements.Length, allElements.Length); + + for (var i = 0; i < elements.Length; i++) + { + elements[i](allElements[i]); + } + }; + + public static Action Image + => o => + { + Assert.NotNull(o); + Assert.IsType(o); + }; + + public static Action ClassifiedText(params ImmutableArray> runs) + => o => + { + Assert.NotNull(o); + var classifiedText = Assert.IsType(o); + + var allRuns = classifiedText.Runs.ToArray(); + Assert.Equal(runs.Length, allRuns.Length); + + for (var i = 0; i < runs.Length; i++) + { + runs[i](allRuns[i]); + } + }; + + public static Action Run(string text, string? classificationTypeName = null) + => run => + { + if (classificationTypeName is not null) + { + Assert.Equal(classificationTypeName, run.ClassificationTypeName); + } + + Assert.Equal(text, run.Text); + }; + + public static Action Keyword(string text) + => Run(text, ClassificationTypeNames.Keyword); + + public static Action LocalName(string text) + => Run(text, ClassificationTypeNames.LocalName); + + public static Action Punctuation(string text) + => Run(text, ClassificationTypeNames.Punctuation); + + public static Action Text(string text) + => Run(text, ClassificationTypeNames.Text); + + public static Action Type(string text) + => Run(text, ClassifiedTagHelperTooltipFactory.TypeClassificationName); + + public static Action WhiteSpace(string text) + => Run(text, ClassificationTypeNames.WhiteSpace); +} diff --git a/src/Shared/Microsoft.AspNetCore.Razor.Utilities.Shared/Assumed.cs b/src/Shared/Microsoft.AspNetCore.Razor.Utilities.Shared/Assumed.cs index 8493bbb768..5b32f5523e 100644 --- a/src/Shared/Microsoft.AspNetCore.Razor.Utilities.Shared/Assumed.cs +++ b/src/Shared/Microsoft.AspNetCore.Razor.Utilities.Shared/Assumed.cs @@ -40,22 +40,37 @@ internal static class Assumed public static void Unreachable([CallerFilePath] string? path = null, [CallerLineNumber] int line = 0) => ThrowInvalidOperation(SR.This_program_location_is_thought_to_be_unreachable, path, line); + /// + /// Can be called at points that are assumed to be unreachable at runtime. + /// + /// + [DoesNotReturn] + public static void Unreachable(string message, [CallerFilePath] string? path = null, [CallerLineNumber] int line = 0) + => ThrowInvalidOperation(message, path, line); + /// /// Can be called at points that are assumed to be unreachable at runtime. /// /// [DoesNotReturn] public static T Unreachable([CallerFilePath] string? path = null, [CallerLineNumber] int line = 0) - { - ThrowInvalidOperation(SR.This_program_location_is_thought_to_be_unreachable, path, line); - return default; - } + => ThrowInvalidOperation(SR.This_program_location_is_thought_to_be_unreachable, path, line); + + /// + /// Can be called at points that are assumed to be unreachable at runtime. + /// + /// + [DoesNotReturn] + public static T Unreachable(string message, [CallerFilePath] string? path = null, [CallerLineNumber] int line = 0) + => ThrowInvalidOperation(message, path, line); [DebuggerHidden] [DoesNotReturn] - [MethodImpl(MethodImplOptions.NoInlining)] private static void ThrowInvalidOperation(string message, string? path, int line) - { - throw new InvalidOperationException(message); - } + => ThrowHelper.ThrowInvalidOperationException(message + Environment.NewLine + SR.FormatFile_0_Line_1(path, line)); + + [DebuggerHidden] + [DoesNotReturn] + private static T ThrowInvalidOperation(string message, string? path, int line) + => ThrowHelper.ThrowInvalidOperationException(message + Environment.NewLine + SR.FormatFile_0_Line_1(path, line)); }