diff --git a/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Microbenchmarks/LanguageServer/RazorSemanticTokensBenchmark.cs b/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Microbenchmarks/LanguageServer/RazorSemanticTokensBenchmark.cs index 3dd7dad96c1..f849a43e31a 100644 --- a/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Microbenchmarks/LanguageServer/RazorSemanticTokensBenchmark.cs +++ b/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Microbenchmarks/LanguageServer/RazorSemanticTokensBenchmark.cs @@ -38,8 +38,6 @@ public class RazorSemanticTokensBenchmark : RazorLanguageServerBenchmarkBase private ProjectSnapshotManagerDispatcher ProjectSnapshotManagerDispatcher { get; set; } - private RazorSemanticTokensLegend SemanticTokensLegend { get; set; } - private string PagesDirectory { get; set; } private string ProjectFilePath { get; set; } @@ -81,8 +79,6 @@ public async Task InitializeRazorSemanticAsync() Character = text.Lines.Last().Span.Length - 1 } }; - - SemanticTokensLegend = new RazorSemanticTokensLegend(new VSInternalClientCapabilities() { SupportsVisualStudioExtensions = true }); } [Benchmark(Description = "Razor Semantic Tokens Range Handling")] @@ -94,11 +90,12 @@ public async Task RazorSemanticTokensRangeAsync() }; var cancellationToken = CancellationToken.None; var documentVersion = 1; - var correlationId = Guid.Empty; await UpdateDocumentAsync(documentVersion, DocumentSnapshot, cancellationToken).ConfigureAwait(false); + + var clientConnection = RazorLanguageServer.GetRequiredService(); await RazorSemanticTokenService.GetSemanticTokensAsync( - textDocumentIdentifier, Range, DocumentContext, SemanticTokensLegend, correlationId, cancellationToken).ConfigureAwait(false); + clientConnection, textDocumentIdentifier, Range, DocumentContext, colorBackground: false, cancellationToken).ConfigureAwait(false); } private async Task UpdateDocumentAsync(int newVersion, IDocumentSnapshot documentSnapshot, CancellationToken cancellationToken) @@ -124,6 +121,7 @@ private void EnsureServicesInitialized() { var languageServer = RazorLanguageServer.GetInnerLanguageServerForTesting(); RazorSemanticTokenService = languageServer.GetRequiredService(); + RazorSemanticTokenService.ApplyCapabilities(new(), new VSInternalClientCapabilities { SupportsVisualStudioExtensions = true }); VersionCache = languageServer.GetRequiredService(); ProjectSnapshotManagerDispatcher = languageServer.GetRequiredService(); } @@ -131,21 +129,21 @@ private void EnsureServicesInitialized() internal class TestRazorSemanticTokensInfoService : RazorSemanticTokensInfoService { public TestRazorSemanticTokensInfoService( - IClientConnection clientConnection, LanguageServerFeatureOptions languageServerFeatureOptions, IRazorDocumentMappingService documentMappingService, - RazorLSPOptionsMonitor razorLSPOptionsMonitor, IRazorLoggerFactory loggerFactory) - : base(clientConnection, documentMappingService, razorLSPOptionsMonitor, languageServerFeatureOptions, loggerFactory) + : base(documentMappingService, languageServerFeatureOptions, loggerFactory, telemetryReporter: null) { } // We can't get C# responses without significant amounts of extra work, so let's just shim it for now, any non-Null result is fine. internal override Task?> GetCSharpSemanticRangesAsync( + IClientConnection clientConnection, RazorCodeDocument codeDocument, TextDocumentIdentifier textDocumentIdentifier, Range razorRange, RazorSemanticTokensLegend razorSemanticTokensLegend, + bool colorBackground, long documentVersion, Guid correlationId, CancellationToken cancellationToken, diff --git a/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Microbenchmarks/LanguageServer/RazorSemanticTokensRangeEndpointBenchmark.cs b/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Microbenchmarks/LanguageServer/RazorSemanticTokensRangeEndpointBenchmark.cs index 29410568023..4794a400ec2 100644 --- a/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Microbenchmarks/LanguageServer/RazorSemanticTokensRangeEndpointBenchmark.cs +++ b/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Microbenchmarks/LanguageServer/RazorSemanticTokensRangeEndpointBenchmark.cs @@ -71,7 +71,10 @@ public async Task InitializeRazorSemanticAsync() var documentSnapshot = GetDocumentSnapshot(ProjectFilePath, filePath, TargetPath); var version = 1; DocumentContext = new VersionedDocumentContext(documentUri, documentSnapshot, projectContext: null, version); - SemanticTokensRangeEndpoint = new SemanticTokensRangeEndpoint(telemetryReporter: null); + + var razorOptionsMonitor = RazorLanguageServer.GetRequiredService(); + var clientConnection = RazorLanguageServer.GetRequiredService(); + SemanticTokensRangeEndpoint = new SemanticTokensRangeEndpoint(RazorSemanticTokenService, razorOptionsMonitor, clientConnection); SemanticTokensRangeEndpoint.ApplyCapabilities(new(), new VSInternalClientCapabilities() { SupportsVisualStudioExtensions = true }); var text = await DocumentContext.GetSourceTextAsync(CancellationToken.None).ConfigureAwait(false); @@ -148,21 +151,21 @@ private void EnsureServicesInitialized() internal class TestCustomizableRazorSemanticTokensInfoService : RazorSemanticTokensInfoService { public TestCustomizableRazorSemanticTokensInfoService( - IClientConnection clientConnection, LanguageServerFeatureOptions languageServerFeatureOptions, IRazorDocumentMappingService documentMappingService, - RazorLSPOptionsMonitor razorLSPOptionsMonitor, IRazorLoggerFactory loggerFactory) - : base(clientConnection, documentMappingService, razorLSPOptionsMonitor, languageServerFeatureOptions, loggerFactory) + : base(documentMappingService, languageServerFeatureOptions, loggerFactory, telemetryReporter: null) { } // We can't get C# responses without significant amounts of extra work, so let's just shim it for now, any non-Null result is fine. internal override Task?> GetCSharpSemanticRangesAsync( + IClientConnection clientConnection, RazorCodeDocument codeDocument, TextDocumentIdentifier textDocumentIdentifier, Range razorRange, RazorSemanticTokensLegend razorSemanticTokensLegend, + bool colorBackground, long documentVersion, Guid correlationId, CancellationToken cancellationToken, diff --git a/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Microbenchmarks/LanguageServer/RazorSemanticTokensScrollingBenchmark.cs b/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Microbenchmarks/LanguageServer/RazorSemanticTokensScrollingBenchmark.cs index 86dc7be9611..5d52999e867 100644 --- a/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Microbenchmarks/LanguageServer/RazorSemanticTokensScrollingBenchmark.cs +++ b/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Microbenchmarks/LanguageServer/RazorSemanticTokensScrollingBenchmark.cs @@ -58,6 +58,7 @@ public async Task InitializeRazorSemanticAsync() DocumentContext = new VersionedDocumentContext(documentUri, documentSnapshot, projectContext: null, version: 1); SemanticTokensLegend = new RazorSemanticTokensLegend(new VSInternalClientCapabilities() { SupportsVisualStudioExtensions = true }); + RazorSemanticTokenService.ApplyCapabilities(new(), new VSInternalClientCapabilities() { SupportsVisualStudioExtensions = true }); var text = await DocumentSnapshot.GetTextAsync().ConfigureAwait(false); Range = new Range @@ -85,13 +86,14 @@ public async Task RazorSemanticTokensRangeScrollingAsync() Uri = DocumentUri }; var cancellationToken = CancellationToken.None; - var correlationId = Guid.Empty; var documentVersion = 1; await UpdateDocumentAsync(documentVersion, DocumentSnapshot).ConfigureAwait(false); var documentLineCount = Range.End.Line; + var clientConnection = RazorLanguageServer.GetRequiredService(); + var lineCount = 0; while (lineCount != documentLineCount) { @@ -102,11 +104,11 @@ public async Task RazorSemanticTokensRangeScrollingAsync() End = new Position(newLineCount, 0) }; await RazorSemanticTokenService!.GetSemanticTokensAsync( + clientConnection, textDocumentIdentifier, range, DocumentContext, - SemanticTokensLegend, - correlationId, + colorBackground: false, cancellationToken); lineCount = newLineCount; diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Extensions/IServiceCollectionExtensions.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Extensions/IServiceCollectionExtensions.cs index fe893a1f7cc..d6c76cbc454 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Extensions/IServiceCollectionExtensions.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Extensions/IServiceCollectionExtensions.cs @@ -120,16 +120,19 @@ public static void AddHoverServices(this IServiceCollection services) services.AddSingleton(); } - public static void AddSemanticTokensServices(this IServiceCollection services) + public static void AddSemanticTokensServices(this IServiceCollection services, LanguageServerFeatureOptions featureOptions) { - services.AddHandlerWithCapabilities(); + if (!featureOptions.UseRazorCohostServer) + { + services.AddHandlerWithCapabilities(); + // Ensure that we don't add the default service if something else has added one. + services.TryAddSingleton(); + } + services.AddHandler(); services.AddSingleton(); services.AddSingleton(); - - // Ensure that we don't add the default service if something else has added one. - services.TryAddSingleton(); } public static void AddCodeActionsServices(this IServiceCollection services) diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorLanguageServer.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorLanguageServer.cs index 4c7b4d420ba..4f277fd9cc0 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorLanguageServer.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorLanguageServer.cs @@ -127,7 +127,7 @@ protected override ILspServices ConstructLspServices() services.AddLifeCycleServices(this, _clientConnection, _lspServerActivationTracker); services.AddDiagnosticServices(); - services.AddSemanticTokensServices(); + services.AddSemanticTokensServices(featureOptions); services.AddDocumentManagementServices(featureOptions); services.AddCompletionServices(featureOptions); services.AddFormattingServices(); diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Semantic/SemanticTokensRangeEndpoint.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Semantic/SemanticTokensRangeEndpoint.cs index 920d3c5310a..56374e87f89 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Semantic/SemanticTokensRangeEndpoint.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Semantic/SemanticTokensRangeEndpoint.cs @@ -1,43 +1,30 @@ // 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; using System.Threading; using System.Threading.Tasks; -using Microsoft.AspNetCore.Razor.LanguageServer.Common; using Microsoft.AspNetCore.Razor.LanguageServer.EndpointContracts; -using Microsoft.AspNetCore.Razor.Telemetry; using Microsoft.CommonLanguageServerProtocol.Framework; -using Microsoft.Extensions.Logging; using Microsoft.VisualStudio.LanguageServer.Protocol; namespace Microsoft.AspNetCore.Razor.LanguageServer.Semantic; -[LanguageServerEndpoint(LspEndpointName)] -internal sealed class SemanticTokensRangeEndpoint : IRazorRequestHandler, ICapabilitiesProvider +[LanguageServerEndpoint(Methods.TextDocumentSemanticTokensRangeName)] +internal sealed class SemanticTokensRangeEndpoint( + IRazorSemanticTokensInfoService semanticTokensInfoService, + RazorLSPOptionsMonitor razorLSPOptionsMonitor, + IClientConnection clientConnection) + : IRazorRequestHandler, ICapabilitiesProvider { - public const string LspEndpointName = Methods.TextDocumentSemanticTokensRangeName; - private RazorSemanticTokensLegend? _razorSemanticTokensLegend; - private readonly ITelemetryReporter? _telemetryReporter; - - public SemanticTokensRangeEndpoint(ITelemetryReporter? telemetryReporter) - { - _telemetryReporter = telemetryReporter; - } + private readonly IRazorSemanticTokensInfoService _semanticTokensInfoService = semanticTokensInfoService; + private readonly RazorLSPOptionsMonitor _razorLSPOptionsMonitor = razorLSPOptionsMonitor; + private readonly IClientConnection _clientConnection = clientConnection; public bool MutatesSolutionState { get; } = false; public void ApplyCapabilities(VSInternalServerCapabilities serverCapabilities, VSInternalClientCapabilities clientCapabilities) { - _razorSemanticTokensLegend = new RazorSemanticTokensLegend(clientCapabilities); - - serverCapabilities.SemanticTokensOptions = new SemanticTokensOptions - { - Full = false, - Legend = _razorSemanticTokensLegend.Legend, - Range = true, - }; + _semanticTokensInfoService.ApplyCapabilities(serverCapabilities, clientCapabilities); } public TextDocumentIdentifier GetTextDocumentIdentifier(SemanticTokensRangeParams request) @@ -47,26 +34,10 @@ public TextDocumentIdentifier GetTextDocumentIdentifier(SemanticTokensRangeParam public async Task HandleRequestAsync(SemanticTokensRangeParams request, RazorRequestContext requestContext, CancellationToken cancellationToken) { - if (request is null) - { - throw new ArgumentNullException(nameof(request)); - } - var documentContext = requestContext.GetRequiredDocumentContext(); - var semanticTokensInfoService = requestContext.GetRequiredService(); - - var correlationId = Guid.NewGuid(); - using var _ = _telemetryReporter?.TrackLspRequest(LspEndpointName, LanguageServerConstants.RazorLanguageServerName, correlationId); - var semanticTokens = await semanticTokensInfoService.GetSemanticTokensAsync(request.TextDocument, request.Range, documentContext, _razorSemanticTokensLegend.AssumeNotNull(), correlationId, cancellationToken).ConfigureAwait(false); - var amount = semanticTokens is null ? "no" : (semanticTokens.Data.Length / 5).ToString(Thread.CurrentThread.CurrentCulture); - - requestContext.Logger.LogInformation("Returned {amount} semantic tokens for range ({startLine},{startChar})-({endLine},{endChar}) in {request.TextDocument.Uri}.", amount, request.Range.Start.Line, request.Range.Start.Character, request.Range.End.Line, request.Range.End.Character, request.TextDocument.Uri); + var colorBackground = _razorLSPOptionsMonitor.CurrentValue.ColorBackground; - if (semanticTokens is not null) - { - Debug.Assert(semanticTokens.Data.Length % 5 == 0, $"Number of semantic token-ints should be divisible by 5. Actual number: {semanticTokens.Data.Length}"); - Debug.Assert(semanticTokens.Data.Length == 0 || semanticTokens.Data[0] >= 0, $"Line offset should not be negative."); - } + var semanticTokens = await _semanticTokensInfoService.GetSemanticTokensAsync(_clientConnection, request.TextDocument, request.Range, documentContext, colorBackground, cancellationToken).ConfigureAwait(false); return semanticTokens; } diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Semantic/Services/IRazorSemanticTokenInfoService.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Semantic/Services/IRazorSemanticTokenInfoService.cs index a506293458a..2029c027edb 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Semantic/Services/IRazorSemanticTokenInfoService.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Semantic/Services/IRazorSemanticTokenInfoService.cs @@ -1,14 +1,13 @@ // 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; using System.Threading.Tasks; using Microsoft.VisualStudio.LanguageServer.Protocol; namespace Microsoft.AspNetCore.Razor.LanguageServer.Semantic; -internal interface IRazorSemanticTokensInfoService +internal interface IRazorSemanticTokensInfoService : ICapabilitiesProvider { - Task GetSemanticTokensAsync(TextDocumentIdentifier textDocumentIdentifier, Range range, VersionedDocumentContext documentContext, RazorSemanticTokensLegend razorSemanticTokensLegend, Guid correlationId, CancellationToken cancellationToken); + Task GetSemanticTokensAsync(IClientConnection clientConnection, TextDocumentIdentifier textDocumentIdentifier, Range range, VersionedDocumentContext documentContext, bool colorBackground, CancellationToken cancellationToken); } diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Semantic/Services/RazorSemanticTokensInfoService.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Semantic/Services/RazorSemanticTokensInfoService.cs index c6ce6e0d769..82633a2648b 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Semantic/Services/RazorSemanticTokensInfoService.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Semantic/Services/RazorSemanticTokensInfoService.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; +using System.Composition; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Linq; @@ -14,6 +15,7 @@ using Microsoft.AspNetCore.Razor.LanguageServer.Extensions; using Microsoft.AspNetCore.Razor.LanguageServer.Semantic.Models; using Microsoft.AspNetCore.Razor.PooledObjects; +using Microsoft.AspNetCore.Razor.Telemetry; using Microsoft.CodeAnalysis.Razor; using Microsoft.CodeAnalysis.Razor.Logging; using Microsoft.CodeAnalysis.Razor.Workspaces; @@ -24,53 +26,83 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer.Semantic; -internal class RazorSemanticTokensInfoService : IRazorSemanticTokensInfoService +[Export(typeof(IRazorSemanticTokensInfoService)), Shared] +[method: ImportingConstructor] +internal class RazorSemanticTokensInfoService( + IRazorDocumentMappingService documentMappingService, + LanguageServerFeatureOptions languageServerFeatureOptions, + IRazorLoggerFactory loggerFactory, + ITelemetryReporter? telemetryReporter) + : IRazorSemanticTokensInfoService { private const int TokenSize = 5; - private readonly IRazorDocumentMappingService _documentMappingService; - private readonly LanguageServerFeatureOptions _languageServerFeatureOptions; - private readonly RazorLSPOptionsMonitor _razorLSPOptionsMonitor; - private readonly IClientConnection _clientConnection; - private readonly ILogger _logger; + private readonly IRazorDocumentMappingService _documentMappingService = documentMappingService ?? throw new ArgumentNullException(nameof(documentMappingService)); + private readonly LanguageServerFeatureOptions _languageServerFeatureOptions = languageServerFeatureOptions ?? throw new ArgumentNullException(nameof(languageServerFeatureOptions)); + private readonly ILogger _logger = loggerFactory.CreateLogger(); + private readonly ITelemetryReporter? _telemetryReporter = telemetryReporter; - public RazorSemanticTokensInfoService( + private RazorSemanticTokensLegend? _razorSemanticTokensLegend; + + public void ApplyCapabilities(VSInternalServerCapabilities serverCapabilities, VSInternalClientCapabilities clientCapabilities) + { + _razorSemanticTokensLegend = new RazorSemanticTokensLegend(clientCapabilities); + + serverCapabilities.SemanticTokensOptions = new SemanticTokensOptions + { + Full = false, + Legend = _razorSemanticTokensLegend.Legend, + Range = true, + }; + } + + public async Task GetSemanticTokensAsync( IClientConnection clientConnection, - IRazorDocumentMappingService documentMappingService, - RazorLSPOptionsMonitor razorLSPOptionsMonitor, - LanguageServerFeatureOptions languageServerFeatureOptions, - IRazorLoggerFactory loggerFactory) + TextDocumentIdentifier textDocumentIdentifier, + Range range, + VersionedDocumentContext documentContext, + bool colorBackground, + CancellationToken cancellationToken) { - _clientConnection = clientConnection ?? throw new ArgumentNullException(nameof(clientConnection)); - _documentMappingService = documentMappingService ?? throw new ArgumentNullException(nameof(documentMappingService)); - _razorLSPOptionsMonitor = razorLSPOptionsMonitor ?? throw new ArgumentNullException(nameof(razorLSPOptionsMonitor)); - _languageServerFeatureOptions = languageServerFeatureOptions ?? throw new ArgumentNullException(nameof(languageServerFeatureOptions)); + _razorSemanticTokensLegend.AssumeNotNull(); - if (loggerFactory is null) + var correlationId = Guid.NewGuid(); + using var _ = _telemetryReporter?.TrackLspRequest(Methods.TextDocumentSemanticTokensRangeName, LanguageServerConstants.RazorLanguageServerName, correlationId); + + var semanticTokens = await GetSemanticTokensAsync(clientConnection, textDocumentIdentifier, range, documentContext, correlationId, colorBackground, cancellationToken).ConfigureAwait(false); + + var amount = semanticTokens is null ? "no" : (semanticTokens.Data.Length / 5).ToString(Thread.CurrentThread.CurrentCulture); + + _logger.LogInformation("Returned {amount} semantic tokens for range ({startLine},{startChar})-({endLine},{endChar}) in {request.TextDocument.Uri}.", amount, range.Start.Line, range.Start.Character, range.End.Line, range.End.Character, textDocumentIdentifier.Uri); + + if (semanticTokens is not null) { - throw new ArgumentNullException(nameof(loggerFactory)); + Debug.Assert(semanticTokens.Data.Length % 5 == 0, $"Number of semantic token-ints should be divisible by 5. Actual number: {semanticTokens.Data.Length}"); + Debug.Assert(semanticTokens.Data.Length == 0 || semanticTokens.Data[0] >= 0, $"Line offset should not be negative."); } - _logger = loggerFactory.CreateLogger(); + return semanticTokens; } - public async Task GetSemanticTokensAsync( + private async Task GetSemanticTokensAsync( + IClientConnection clientConnection, TextDocumentIdentifier textDocumentIdentifier, Range range, VersionedDocumentContext documentContext, - RazorSemanticTokensLegend razorSemanticTokensLegend, Guid correlationId, + bool colorBackground, CancellationToken cancellationToken) { var codeDocument = await documentContext.GetCodeDocumentAsync(cancellationToken).ConfigureAwait(false); cancellationToken.ThrowIfCancellationRequested(); - var razorSemanticRanges = TagHelperSemanticRangeVisitor.VisitAllNodes(codeDocument, range, razorSemanticTokensLegend, _razorLSPOptionsMonitor.CurrentValue.ColorBackground); + + var razorSemanticRanges = TagHelperSemanticRangeVisitor.VisitAllNodes(codeDocument, range, _razorSemanticTokensLegend.AssumeNotNull(), colorBackground); ImmutableArray? csharpSemanticRangesResult = null; try { - csharpSemanticRangesResult = await GetCSharpSemanticRangesAsync(codeDocument, textDocumentIdentifier, range, razorSemanticTokensLegend, documentContext.Version, correlationId, cancellationToken).ConfigureAwait(false); + csharpSemanticRangesResult = await GetCSharpSemanticRangesAsync(clientConnection, codeDocument, textDocumentIdentifier, range, _razorSemanticTokensLegend, colorBackground, documentContext.Version, correlationId, cancellationToken).ConfigureAwait(false); } catch (OperationCanceledException) { @@ -133,10 +165,12 @@ private static ImmutableArray CombineSemanticRanges(ImmutableArra // Internal and virtual for testing only internal virtual async Task?> GetCSharpSemanticRangesAsync( + IClientConnection clientConnection, RazorCodeDocument codeDocument, TextDocumentIdentifier textDocumentIdentifier, Range razorRange, RazorSemanticTokensLegend razorSemanticTokensLegend, + bool colorBackground, long documentVersion, Guid correlationId, CancellationToken cancellationToken, @@ -171,7 +205,7 @@ private static ImmutableArray CombineSemanticRanges(ImmutableArra csharpRanges = new Range[] { csharpRange }; } - var csharpResponse = await GetMatchingCSharpResponseAsync(textDocumentIdentifier, documentVersion, csharpRanges, correlationId, cancellationToken).ConfigureAwait(false); + var csharpResponse = await GetMatchingCSharpResponseAsync(clientConnection, textDocumentIdentifier, documentVersion, csharpRanges, correlationId, cancellationToken).ConfigureAwait(false); // Indicates an issue with retrieving the C# response (e.g. no response or C# is out of sync with us). // Unrecoverable, return default to indicate no change. We've already queued up a refresh request in @@ -184,7 +218,6 @@ private static ImmutableArray CombineSemanticRanges(ImmutableArra using var _ = ArrayBuilderPool.GetPooledObject(out var razorRanges); razorRanges.SetCapacityIfLarger(csharpResponse.Length / TokenSize); - var colorBackground = _razorLSPOptionsMonitor.CurrentValue.ColorBackground; var textClassification = razorSemanticTokensLegend.MarkupTextLiteral; var razorSource = codeDocument.GetSourceText(); @@ -307,6 +340,7 @@ internal static bool TryGetMinimalCSharpRange(RazorCodeDocument codeDocument, Ra } private async Task GetMatchingCSharpResponseAsync( + IClientConnection clientConnection, TextDocumentIdentifier textDocumentIdentifier, long documentVersion, Range[] csharpRanges, @@ -317,7 +351,7 @@ internal static bool TryGetMinimalCSharpRange(RazorCodeDocument codeDocument, Ra ProvideSemanticTokensResponse? csharpResponse; if (_languageServerFeatureOptions.UsePreciseSemanticTokenRanges) { - csharpResponse = await GetCsharpResponseAsync(parameter, CustomMessageNames.RazorProvidePreciseRangeSemanticTokensEndpoint, cancellationToken).ConfigureAwait(false); + csharpResponse = await GetCsharpResponseAsync(clientConnection, parameter, CustomMessageNames.RazorProvidePreciseRangeSemanticTokensEndpoint, cancellationToken).ConfigureAwait(false); // Likely the server doesn't support the new endpoint, fallback to the original one if (csharpResponse?.Tokens is null && csharpRanges.Length > 1) @@ -334,12 +368,12 @@ internal static bool TryGetMinimalCSharpRange(RazorCodeDocument codeDocument, Ra new[] { minimalRange }, parameter.CorrelationId); - csharpResponse = await GetCsharpResponseAsync(newParams, CustomMessageNames.RazorProvideSemanticTokensRangeEndpoint, cancellationToken).ConfigureAwait(false); + csharpResponse = await GetCsharpResponseAsync(clientConnection, newParams, CustomMessageNames.RazorProvideSemanticTokensRangeEndpoint, cancellationToken).ConfigureAwait(false); } } else { - csharpResponse = await GetCsharpResponseAsync(parameter, CustomMessageNames.RazorProvideSemanticTokensRangeEndpoint, cancellationToken).ConfigureAwait(false); + csharpResponse = await GetCsharpResponseAsync(clientConnection, parameter, CustomMessageNames.RazorProvideSemanticTokensRangeEndpoint, cancellationToken).ConfigureAwait(false); } if (csharpResponse is null) @@ -405,12 +439,12 @@ internal static bool TryGetSortedCSharpRanges(RazorCodeDocument codeDocument, Ra return true; } - private async Task GetCsharpResponseAsync(ProvideSemanticTokensRangesParams parameter, string lspMethodName, CancellationToken cancellationToken) + private Task GetCsharpResponseAsync(IClientConnection clientConnection, ProvideSemanticTokensRangesParams parameter, string lspMethodName, CancellationToken cancellationToken) { - return await _clientConnection.SendRequestAsync( + return clientConnection.SendRequestAsync( lspMethodName, parameter, - cancellationToken).ConfigureAwait(false); + cancellationToken); } private static SemanticRange CSharpDataToSemanticRange( diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/Cohost/CohostSemanticTokensRangeEndpoint.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/Cohost/CohostSemanticTokensRangeEndpoint.cs new file mode 100644 index 00000000000..d47819a04d6 --- /dev/null +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/Cohost/CohostSemanticTokensRangeEndpoint.cs @@ -0,0 +1,67 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using System.Composition; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Razor; +using Microsoft.AspNetCore.Razor.LanguageServer; +using Microsoft.AspNetCore.Razor.LanguageServer.Semantic; +using Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost; +using Microsoft.CodeAnalysis.Razor.Logging; +using Microsoft.CommonLanguageServerProtocol.Framework; +using Microsoft.Extensions.Logging; +using Microsoft.VisualStudio.Editor.Razor; +using Microsoft.VisualStudio.LanguageServer.Protocol; +using Microsoft.VisualStudio.LanguageServerClient.Razor.Extensions; + +namespace Microsoft.VisualStudio.LanguageServerClient.Razor.Cohost; + +[LanguageServerEndpoint(Methods.TextDocumentSemanticTokensRangeName)] +[ExportRazorStatelessLspService(typeof(CohostSemanticTokensRangeEndpoint))] +[Export(typeof(ICapabilitiesProvider))] +[method: ImportingConstructor] +internal sealed class CohostSemanticTokensRangeEndpoint( + IRazorSemanticTokensInfoService semanticTokensInfoService, + IClientSettingsManager clientSettingsManager, + IDocumentContextFactory documentContextFactory, + IRazorLoggerFactory loggerFactory) + : AbstractRazorCohostDocumentRequestHandler, ICapabilitiesProvider +{ + private readonly IRazorSemanticTokensInfoService _semanticTokensInfoService = semanticTokensInfoService; + private readonly IClientSettingsManager _clientSettingsManager = clientSettingsManager; + private readonly IDocumentContextFactory _documentContextFactory = documentContextFactory; + private readonly ILogger _logger = loggerFactory.CreateLogger(); + + protected override bool MutatesSolutionState => false; + protected override bool RequiresLSPSolution => true; + + protected override RazorTextDocumentIdentifier? GetRazorTextDocumentIdentifier(SemanticTokensRangeParams request) + => request.TextDocument.ToRazorTextDocumentIdentifier(); + + public void ApplyCapabilities(VSInternalServerCapabilities serverCapabilities, VSInternalClientCapabilities clientCapabilities) + => _semanticTokensInfoService.ApplyCapabilities(serverCapabilities, clientCapabilities); + + protected override Task HandleRequestAsync(SemanticTokensRangeParams request, RazorCohostRequestContext context, CancellationToken cancellationToken) + { + // TODO: Create document context from request.TextDocument, by looking at request.Solution instead of our project snapshots + var documentContext = _documentContextFactory.TryCreateForOpenDocument(request.TextDocument); + + _logger.LogDebug("[Cohost] Received semantic range request for {requestPath} and got document {documentPath}", request.TextDocument.Uri, documentContext?.FilePath); + + // TODO: We can't MEF import IRazorCohostClientLanguageServerManager in the constructor. We can make this work + // by having it implement a base class, RazorClientConnectionBase or something, that in turn implements + // AbstractRazorLspService (defined in Roslyn) and then move everything from importing IClientConnection + // to importing the new base class, so we can continue to share services. + // + // Until then we have to get the service from the request context. + var clientLanguageServerManager = context.GetRequiredService(); + var clientConnection = new RazorCohostClientConnection(clientLanguageServerManager); + + // TODO: This is currently using the "VS" client settings manager, since that's where we are running. In future + // we should create a hook into Roslyn's LSP options infra so we get the option values from the LSP client + var colorBackground = _clientSettingsManager.GetClientSettings().AdvancedSettings.ColorBackground; + + return _semanticTokensInfoService.GetSemanticTokensAsync(clientConnection, request.TextDocument, request.Range, documentContext.AssumeNotNull(), colorBackground, cancellationToken); + } +} diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Semantic/SemanticTokensTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Semantic/SemanticTokensTest.cs index 5cd3f012aaf..88ff982bbc6 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Semantic/SemanticTokensTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Semantic/SemanticTokensTest.cs @@ -918,7 +918,7 @@ private async Task AssertSemanticTokensAsync( var service = await CreateServiceAsync(documentContext, csharpTokens, withCSharpBackground, serverSupportsPreciseRanges, precise); var range = GetRange(documentText); - var tokens = await service.GetSemanticTokensAsync(new() { Uri = documentContext.Uri }, range, documentContext, TestRazorSemanticTokensLegend.Instance, Guid.Empty, DisposalToken); + var tokens = await service.GetSemanticTokensAsync(_clientConnection.Object, new() { Uri = documentContext.Uri }, range, documentContext, withCSharpBackground, DisposalToken); var sourceText = await documentContext.GetSourceTextAsync(DisposalToken); AssertSemanticTokensMatchesBaseline(sourceText, tokens?.Data, testName.AssumeNotNull()); @@ -1006,12 +1006,13 @@ private async Task CreateServiceAsync( options.HtmlVirtualDocumentSuffix == "__virtual.html", MockBehavior.Strict); - return new RazorSemanticTokensInfoService( - _clientConnection.Object, + var service = new RazorSemanticTokensInfoService( documentMappingService, - optionsMonitor, featureOptions, - LoggerFactory); + LoggerFactory, + telemetryReporter: null); + service.ApplyCapabilities(new(), new VSInternalClientCapabilities { SupportsVisualStudioExtensions = true }); + return service; } private async Task GetCSharpSemanticTokensResponseAsync(string documentText, bool precise, bool isRazorFile = false)