From 8a5839e79c6ef58dd7cea2fd5e0b516997b7fb99 Mon Sep 17 00:00:00 2001 From: David Wengier Date: Wed, 6 Mar 2024 14:47:23 +1100 Subject: [PATCH] Rejig semantic tokens service to not take VS protocol types, and extract C# functionality to a separate service --- .../RazorSemanticTokensBenchmark.cs | 23 +-- ...zorSemanticTokensRangeEndpointBenchmark.cs | 17 +- .../RazorSemanticTokensScrollingBenchmark.cs | 18 +- .../IServiceCollectionExtensions.cs | 1 + .../Extensions/RazorCodeDocumentExtensions.cs | 12 +- .../InlayHints/InlayHintService.cs | 8 +- .../Semantic/SemanticTokensRangeEndpoint.cs | 22 ++- .../Services/ICSharpSemanticTokensProvider.cs | 20 ++ .../IRazorSemanticTokenInfoService.cs | 5 +- .../LSPCSharpSemanticTokensProvider.cs | 106 +++++++++++ .../RazorSemanticTokensInfoService.cs | 172 +++++------------- .../Services/SemanticTokensVisitor.cs | 8 +- .../InlayHints/InlayHintEndpointTest.cs | 3 +- .../Semantic/SemanticTokensTest.cs | 40 ++-- 14 files changed, 245 insertions(+), 210 deletions(-) create mode 100644 src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Semantic/Services/ICSharpSemanticTokensProvider.cs create mode 100644 src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Semantic/Services/LSPCSharpSemanticTokensProvider.cs 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 f99f89c0b6a..e314f1c521e 100644 --- a/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Microbenchmarks/LanguageServer/RazorSemanticTokensBenchmark.cs +++ b/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Microbenchmarks/LanguageServer/RazorSemanticTokensBenchmark.cs @@ -12,11 +12,13 @@ using BenchmarkDotNet.Attributes; using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.LanguageServer; +using Microsoft.AspNetCore.Razor.LanguageServer.Extensions; using Microsoft.AspNetCore.Razor.LanguageServer.Semantic; using Microsoft.CodeAnalysis.Razor; using Microsoft.CodeAnalysis.Razor.Logging; using Microsoft.CodeAnalysis.Razor.ProjectSystem; using Microsoft.CodeAnalysis.Razor.Workspaces; +using Microsoft.CodeAnalysis.Text; using Microsoft.Extensions.DependencyInjection; using Microsoft.VisualStudio.LanguageServer.Protocol; @@ -84,18 +86,12 @@ public async Task InitializeRazorSemanticAsync() [Benchmark(Description = "Razor Semantic Tokens Range Handling")] public async Task RazorSemanticTokensRangeAsync() { - var textDocumentIdentifier = new TextDocumentIdentifier - { - Uri = DocumentUri - }; var cancellationToken = CancellationToken.None; var documentVersion = 1; await UpdateDocumentAsync(documentVersion, DocumentSnapshot, cancellationToken).ConfigureAwait(false); - var clientConnection = RazorLanguageServer.GetRequiredService(); - await RazorSemanticTokenService.GetSemanticTokensAsync( - clientConnection, textDocumentIdentifier, Range, DocumentContext, colorBackground: false, cancellationToken).ConfigureAwait(false); + await RazorSemanticTokenService.GetSemanticTokensAsync(DocumentContext, Range.ToLinePositionSpan(), colorBackground: false, Guid.Empty, cancellationToken: cancellationToken).ConfigureAwait(false); } private async Task UpdateDocumentAsync(int newVersion, IDocumentSnapshot documentSnapshot, CancellationToken cancellationToken) @@ -134,21 +130,18 @@ public TestRazorSemanticTokensInfoService( IRazorDocumentMappingService documentMappingService, RazorSemanticTokensLegendService razorSemanticTokensLegendService, IRazorLoggerFactory loggerFactory) - : base(documentMappingService, razorSemanticTokensLegendService, languageServerFeatureOptions, loggerFactory, telemetryReporter: null) + : base(documentMappingService, razorSemanticTokensLegendService, csharpSemanticTokensProvider: null!, languageServerFeatureOptions, loggerFactory) { } // 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, + protected override Task?> GetCSharpSemanticRangesAsync( + VersionedDocumentContext documentContext, RazorCodeDocument codeDocument, - TextDocumentIdentifier textDocumentIdentifier, - Range razorRange, + LinePositionSpan razorRange, bool colorBackground, - long documentVersion, Guid correlationId, - CancellationToken cancellationToken, - string previousResultId = null) + CancellationToken cancellationToken) { return Task.FromResult?>(ImmutableArray.Empty); } 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 c69fa9b9392..fa9086d27b9 100644 --- a/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Microbenchmarks/LanguageServer/RazorSemanticTokensRangeEndpointBenchmark.cs +++ b/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Microbenchmarks/LanguageServer/RazorSemanticTokensRangeEndpointBenchmark.cs @@ -18,6 +18,7 @@ using Microsoft.CodeAnalysis.Razor.Logging; using Microsoft.CodeAnalysis.Razor.ProjectSystem; using Microsoft.CodeAnalysis.Razor.Workspaces; +using Microsoft.CodeAnalysis.Text; using Microsoft.Extensions.DependencyInjection; using Microsoft.VisualStudio.LanguageServer.Protocol; @@ -75,10 +76,9 @@ public async Task InitializeRazorSemanticAsync() DocumentContext = new VersionedDocumentContext(documentUri, documentSnapshot, projectContext: null, version); var razorOptionsMonitor = RazorLanguageServer.GetRequiredService(); - var clientConnection = RazorLanguageServer.GetRequiredService(); var clientCapabilitiesService = new BenchmarkClientCapabilitiesService(new VSInternalClientCapabilities() { SupportsVisualStudioExtensions = true }); var razorSemanticTokensLegendService = new RazorSemanticTokensLegendService(clientCapabilitiesService); - SemanticTokensRangeEndpoint = new SemanticTokensRangeEndpoint(RazorSemanticTokenService, razorSemanticTokensLegendService, razorOptionsMonitor, clientConnection); + SemanticTokensRangeEndpoint = new SemanticTokensRangeEndpoint(RazorSemanticTokenService, razorSemanticTokensLegendService, razorOptionsMonitor, telemetryReporter: null); var text = await DocumentContext.GetSourceTextAsync(CancellationToken.None).ConfigureAwait(false); Range = new Range @@ -158,21 +158,18 @@ public TestCustomizableRazorSemanticTokensInfoService( IRazorDocumentMappingService documentMappingService, RazorSemanticTokensLegendService razorSemanticTokensLegendService, IRazorLoggerFactory loggerFactory) - : base(documentMappingService, razorSemanticTokensLegendService, languageServerFeatureOptions, loggerFactory, telemetryReporter: null) + : base(documentMappingService, razorSemanticTokensLegendService, csharpSemanticTokensProvider: null!, languageServerFeatureOptions, loggerFactory) { } // 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, + protected override Task?> GetCSharpSemanticRangesAsync( + VersionedDocumentContext documentContext, RazorCodeDocument codeDocument, - TextDocumentIdentifier textDocumentIdentifier, - Range razorRange, + LinePositionSpan razorSpan, bool colorBackground, - long documentVersion, Guid correlationId, - CancellationToken cancellationToken, - string previousResultId = null) + CancellationToken cancellationToken) { return Task.FromResult?>(PregeneratedRandomSemanticRanges); } 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 9b8ba309871..f4e41e7686a 100644 --- a/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Microbenchmarks/LanguageServer/RazorSemanticTokensScrollingBenchmark.cs +++ b/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Microbenchmarks/LanguageServer/RazorSemanticTokensScrollingBenchmark.cs @@ -12,6 +12,7 @@ using Microsoft.AspNetCore.Razor.LanguageServer.Semantic; using Microsoft.CodeAnalysis.Razor; using Microsoft.CodeAnalysis.Razor.ProjectSystem; +using Microsoft.CodeAnalysis.Text; using Microsoft.Extensions.DependencyInjection; using Microsoft.VisualStudio.LanguageServer.Protocol; using static Microsoft.AspNetCore.Razor.Microbenchmarks.LanguageServer.RazorSemanticTokensBenchmark; @@ -76,10 +77,6 @@ public async Task InitializeRazorSemanticAsync() [Benchmark(Description = "Razor Semantic Tokens Range Scrolling")] public async Task RazorSemanticTokensRangeScrollingAsync() { - var textDocumentIdentifier = new TextDocumentIdentifier() - { - Uri = DocumentUri - }; var cancellationToken = CancellationToken.None; var documentVersion = 1; @@ -87,23 +84,16 @@ public async Task RazorSemanticTokensRangeScrollingAsync() var documentLineCount = Range.End.Line; - var clientConnection = RazorLanguageServer.GetRequiredService(); - var lineCount = 0; while (lineCount != documentLineCount) { var newLineCount = Math.Min(lineCount + WindowSize, documentLineCount); - var range = new Range - { - Start = new Position(lineCount, 0), - End = new Position(newLineCount, 0) - }; + var span = new LinePositionSpan(new LinePosition(lineCount, 0), new LinePosition(newLineCount, 0)); await RazorSemanticTokenService!.GetSemanticTokensAsync( - clientConnection, - textDocumentIdentifier, - range, DocumentContext, + span, colorBackground: false, + Guid.Empty, 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 8105404ef82..f41b4d4c859 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Extensions/IServiceCollectionExtensions.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Extensions/IServiceCollectionExtensions.cs @@ -126,6 +126,7 @@ public static void AddSemanticTokensServices(this IServiceCollection services) services.AddHandlerWithCapabilities(); // Ensure that we don't add the default service if something else has added one. services.TryAddSingleton(); + services.AddSingleton(); services.AddSingleton(); diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Extensions/RazorCodeDocumentExtensions.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Extensions/RazorCodeDocumentExtensions.cs index af3366b49ef..6b2d774c2b4 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Extensions/RazorCodeDocumentExtensions.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Extensions/RazorCodeDocumentExtensions.cs @@ -2,11 +2,11 @@ // Licensed under the MIT license. See License.txt in the project root for license information. using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.LanguageServer.Protocol; using Microsoft.CodeAnalysis.Razor; using Microsoft.CodeAnalysis.Razor.Workspaces.Extensions; +using Microsoft.CodeAnalysis.Text; namespace Microsoft.AspNetCore.Razor.LanguageServer.Extensions; @@ -20,7 +20,7 @@ public static IRazorGeneratedDocument GetGeneratedDocument(this RazorCodeDocumen _ => throw new System.InvalidOperationException(), }; - public static bool TryGetMinimalCSharpRange(this RazorCodeDocument codeDocument, Range razorRange, [NotNullWhen(true)] out Range? csharpRange) + public static bool TryGetMinimalCSharpRange(this RazorCodeDocument codeDocument, LinePositionSpan razorRange, out LinePositionSpan csharpRange) { SourceSpan? minGeneratedSpan = null; SourceSpan? maxGeneratedSpan = null; @@ -53,16 +53,16 @@ public static bool TryGetMinimalCSharpRange(this RazorCodeDocument codeDocument, if (minGeneratedSpan is not null && maxGeneratedSpan is not null) { var csharpSourceText = codeDocument.GetCSharpSourceText(); - var startRange = minGeneratedSpan.Value.ToRange(csharpSourceText); - var endRange = maxGeneratedSpan.Value.ToRange(csharpSourceText); + var startRange = minGeneratedSpan.Value.ToLinePositionSpan(csharpSourceText); + var endRange = maxGeneratedSpan.Value.ToLinePositionSpan(csharpSourceText); - csharpRange = new Range { Start = startRange.Start, End = endRange.End }; + csharpRange = new LinePositionSpan(startRange.Start, endRange.End); Debug.Assert(csharpRange.Start.CompareTo(csharpRange.End) <= 0, "Range.Start should not be larger than Range.End"); return true; } - csharpRange = null; + csharpRange = default; return false; } } diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlayHints/InlayHintService.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlayHints/InlayHintService.cs index 53bd3a863b9..05a99d97c53 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlayHints/InlayHintService.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlayHints/InlayHintService.cs @@ -27,11 +27,13 @@ internal sealed class InlayHintService(IRazorDocumentMappingService documentMapp var codeDocument = await documentContext.GetCodeDocumentAsync(cancellationToken).ConfigureAwait(false); var csharpDocument = codeDocument.GetCSharpDocument(); + var span = range.ToLinePositionSpan(); + // We are given a range by the client, but our mapping only succeeds if the start and end of the range can both be mapped // to C#. Since that doesn't logically match what we want from inlay hints, we instead get the minimum range of mappable // C# to get hints for. We'll filter that later, to remove the sections that can't be mapped back. - if (!_documentMappingService.TryMapToGeneratedDocumentRange(csharpDocument, range, out var projectedRange) && - !codeDocument.TryGetMinimalCSharpRange(range, out projectedRange)) + if (!_documentMappingService.TryMapToGeneratedDocumentRange(csharpDocument, span, out var projectedLinePositionSpan) && + !codeDocument.TryGetMinimalCSharpRange(span, out projectedLinePositionSpan)) { // There's no C# in the range. return null; @@ -41,7 +43,7 @@ internal sealed class InlayHintService(IRazorDocumentMappingService documentMapp // the results, much like folding ranges. var delegatedRequest = new DelegatedInlayHintParams( Identifier: documentContext.Identifier, - ProjectedRange: projectedRange, + ProjectedRange: projectedLinePositionSpan.ToRange(), ProjectedKind: RazorLanguageKind.CSharp ); 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 c0a7535b5b4..38feb77bff8 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Semantic/SemanticTokensRangeEndpoint.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Semantic/SemanticTokensRangeEndpoint.cs @@ -1,10 +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.AspNetCore.Razor.LanguageServer.Common; using Microsoft.AspNetCore.Razor.LanguageServer.EndpointContracts; +using Microsoft.AspNetCore.Razor.LanguageServer.Extensions; +using Microsoft.AspNetCore.Razor.Telemetry; using Microsoft.VisualStudio.LanguageServer.Protocol; namespace Microsoft.AspNetCore.Razor.LanguageServer.Semantic; @@ -14,13 +17,13 @@ internal sealed class SemanticTokensRangeEndpoint( IRazorSemanticTokensInfoService semanticTokensInfoService, RazorSemanticTokensLegendService razorSemanticTokensLegendService, RazorLSPOptionsMonitor razorLSPOptionsMonitor, - IClientConnection clientConnection) + ITelemetryReporter? telemetryReporter) : IRazorRequestHandler, ICapabilitiesProvider { private readonly IRazorSemanticTokensInfoService _semanticTokensInfoService = semanticTokensInfoService; private readonly RazorSemanticTokensLegendService _razorSemanticTokensLegendService = razorSemanticTokensLegendService; private readonly RazorLSPOptionsMonitor _razorLSPOptionsMonitor = razorLSPOptionsMonitor; - private readonly IClientConnection _clientConnection = clientConnection; + private readonly ITelemetryReporter? _telemetryReporter = telemetryReporter; public bool MutatesSolutionState { get; } = false; @@ -39,8 +42,19 @@ public TextDocumentIdentifier GetTextDocumentIdentifier(SemanticTokensRangeParam var documentContext = requestContext.GetRequiredDocumentContext(); var colorBackground = _razorLSPOptionsMonitor.CurrentValue.ColorBackground; - var semanticTokens = await _semanticTokensInfoService.GetSemanticTokensAsync(_clientConnection, request.TextDocument, request.Range, documentContext, colorBackground, cancellationToken).ConfigureAwait(false); + var correlationId = Guid.NewGuid(); + using var _ = _telemetryReporter?.TrackLspRequest(Methods.TextDocumentSemanticTokensRangeName, LanguageServerConstants.RazorLanguageServerName, correlationId); - return semanticTokens; + var data = await _semanticTokensInfoService.GetSemanticTokensAsync(documentContext, request.Range.ToLinePositionSpan(), colorBackground, correlationId, cancellationToken).ConfigureAwait(false); + + if (data is null) + { + return null; + } + + return new SemanticTokens + { + Data = data, + }; } } diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Semantic/Services/ICSharpSemanticTokensProvider.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Semantic/Services/ICSharpSemanticTokensProvider.cs new file mode 100644 index 00000000000..a8f09151275 --- /dev/null +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Semantic/Services/ICSharpSemanticTokensProvider.cs @@ -0,0 +1,20 @@ +// 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.Immutable; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Text; + +namespace Microsoft.AspNetCore.Razor.LanguageServer.Semantic; + +internal interface ICSharpSemanticTokensProvider +{ + Task GetCSharpSemanticTokensResponseAsync( + VersionedDocumentContext documentContext, + ImmutableArray csharpSpans, + bool usePreciseSemanticTokenRanges, + Guid correlationId, + CancellationToken cancellationToken); +} 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 f3ad1cffbea..a3476831d5d 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,13 +1,14 @@ // 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; +using Microsoft.CodeAnalysis.Text; namespace Microsoft.AspNetCore.Razor.LanguageServer.Semantic; internal interface IRazorSemanticTokensInfoService { - Task GetSemanticTokensAsync(IClientConnection clientConnection, TextDocumentIdentifier textDocumentIdentifier, Range range, VersionedDocumentContext documentContext, bool colorBackground, CancellationToken cancellationToken); + Task GetSemanticTokensAsync(VersionedDocumentContext documentContext, LinePositionSpan range, bool colorBackground, Guid correlationId, CancellationToken cancellationToken); } diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Semantic/Services/LSPCSharpSemanticTokensProvider.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Semantic/Services/LSPCSharpSemanticTokensProvider.cs new file mode 100644 index 00000000000..8edd12a3cfc --- /dev/null +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Semantic/Services/LSPCSharpSemanticTokensProvider.cs @@ -0,0 +1,106 @@ +// 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.Immutable; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Razor.LanguageServer.Common; +using Microsoft.AspNetCore.Razor.LanguageServer.Extensions; +using Microsoft.AspNetCore.Razor.LanguageServer.Semantic.Models; +using Microsoft.AspNetCore.Razor.PooledObjects; +using Microsoft.CodeAnalysis.Razor.Logging; +using Microsoft.CodeAnalysis.Text; +using Microsoft.Extensions.Logging; + +namespace Microsoft.AspNetCore.Razor.LanguageServer.Semantic; + +internal class LSPCSharpSemanticTokensProvider(IClientConnection clientConnection, IRazorLoggerFactory loggerFactory) : ICSharpSemanticTokensProvider +{ + private readonly IClientConnection _clientConnection = clientConnection; + private readonly ILogger _logger = loggerFactory.CreateLogger(); + + public async Task GetCSharpSemanticTokensResponseAsync( + VersionedDocumentContext documentContext, + ImmutableArray csharpSpans, + bool usePreciseSemanticTokenRanges, + Guid correlationId, + CancellationToken cancellationToken) + { + var documentVersion = documentContext.Version; + + using var _ = ListPool.GetPooledObject(out var csharpRangeList); + foreach (var span in csharpSpans) + { + csharpRangeList.Add(span.ToRange()); + } + + var csharpRanges = csharpRangeList.ToArray(); + + var parameter = new ProvideSemanticTokensRangesParams(documentContext.Identifier.TextDocumentIdentifier, documentVersion, csharpRanges, correlationId); + ProvideSemanticTokensResponse? csharpResponse; + if (usePreciseSemanticTokenRanges) + { + 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) + { + var minimalRange = new Range + { + Start = csharpRanges[0].Start, + End = csharpRanges[^1].End + }; + + var newParams = new ProvideSemanticTokensRangesParams( + parameter.TextDocument, + parameter.RequiredHostDocumentVersion, + [minimalRange], + parameter.CorrelationId); + + csharpResponse = await GetCsharpResponseAsync(_clientConnection, newParams, CustomMessageNames.RazorProvideSemanticTokensRangeEndpoint, cancellationToken).ConfigureAwait(false); + } + } + else + { + csharpResponse = await GetCsharpResponseAsync(_clientConnection, parameter, CustomMessageNames.RazorProvideSemanticTokensRangeEndpoint, cancellationToken).ConfigureAwait(false); + } + + if (csharpResponse is null) + { + // C# isn't ready yet, don't make Razor wait for it. Once C# is ready they'll send a refresh notification. + return []; + } + + var csharpVersion = csharpResponse.HostDocumentSyncVersion; + if (csharpVersion != documentVersion) + { + // No C# response or C# is out of sync with us. Unrecoverable, return null to indicate no change. + // Once C# syncs up they'll send a refresh notification. + if (csharpVersion == -1) + { + _logger.LogWarning("Didn't get C# tokens because the virtual document wasn't found, or other problem. We were wanting {documentVersion} but C# could not get any version.", documentVersion); + } + else if (csharpVersion < documentVersion) + { + _logger.LogDebug("Didn't wait for Roslyn to get the C# version we were expecting. We are wanting {documentVersion} but C# is at {csharpVersion}.", documentVersion, csharpVersion); + } + else + { + _logger.LogWarning("We are behind the C# version which is surprising. Could be an old request that wasn't cancelled, but if not, expect most future requests to fail. We were wanting {documentVersion} but C# is at {csharpVersion}.", documentVersion, csharpVersion); + } + + return null; + } + + return csharpResponse.Tokens ?? []; + } + + private static Task GetCsharpResponseAsync(IClientConnection clientConnection, ProvideSemanticTokensRangesParams parameter, string lspMethodName, CancellationToken cancellationToken) + { + return clientConnection.SendRequestAsync( + lspMethodName, + parameter, + 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 15cac7ca02a..d22e69c4c8b 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 @@ -6,23 +6,18 @@ using System.Collections.Immutable; using System.Composition; using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.Language; -using Microsoft.AspNetCore.Razor.LanguageServer.Common; 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; using Microsoft.CodeAnalysis.Razor.Workspaces.Extensions; using Microsoft.CodeAnalysis.Text; using Microsoft.Extensions.Logging; -using Microsoft.VisualStudio.LanguageServer.Protocol; namespace Microsoft.AspNetCore.Razor.LanguageServer.Semantic; @@ -31,50 +26,44 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer.Semantic; internal class RazorSemanticTokensInfoService( IRazorDocumentMappingService documentMappingService, RazorSemanticTokensLegendService razorSemanticTokensLegendService, + ICSharpSemanticTokensProvider csharpSemanticTokensProvider, LanguageServerFeatureOptions languageServerFeatureOptions, - IRazorLoggerFactory loggerFactory, - ITelemetryReporter? telemetryReporter) + IRazorLoggerFactory loggerFactory) : IRazorSemanticTokensInfoService { private const int TokenSize = 5; private readonly IRazorDocumentMappingService _documentMappingService = documentMappingService; private readonly RazorSemanticTokensLegendService _razorSemanticTokensLegendService = razorSemanticTokensLegendService; + private readonly ICSharpSemanticTokensProvider _csharpSemanticTokensProvider = csharpSemanticTokensProvider; private readonly LanguageServerFeatureOptions _languageServerFeatureOptions = languageServerFeatureOptions; private readonly ILogger _logger = loggerFactory.CreateLogger(); - private readonly ITelemetryReporter? _telemetryReporter = telemetryReporter; - public async Task GetSemanticTokensAsync( - IClientConnection clientConnection, - TextDocumentIdentifier textDocumentIdentifier, - Range range, + public async Task GetSemanticTokensAsync( VersionedDocumentContext documentContext, + LinePositionSpan span, bool colorBackground, + Guid correlationId, CancellationToken cancellationToken) { - 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 semanticTokens = await GetSemanticTokensAsync(documentContext, span, correlationId, colorBackground, cancellationToken).ConfigureAwait(false); - var amount = semanticTokens is null ? "no" : (semanticTokens.Data.Length / TokenSize).ToString(Thread.CurrentThread.CurrentCulture); + var amount = semanticTokens is null ? "no" : (semanticTokens.Length / TokenSize).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); + _logger.LogInformation("Returned {amount} semantic tokens for span {span} in {request.TextDocument.Uri}.", amount, span, documentContext.Uri); if (semanticTokens is not null) { - Debug.Assert(semanticTokens.Data.Length % TokenSize == 0, $"Number of semantic token-ints should be divisible by {TokenSize}. Actual number: {semanticTokens.Data.Length}"); - Debug.Assert(semanticTokens.Data.Length == 0 || semanticTokens.Data[0] >= 0, $"Line offset should not be negative."); + Debug.Assert(semanticTokens.Length % TokenSize == 0, $"Number of semantic token-ints should be divisible by {TokenSize}. Actual number: {semanticTokens.Length}"); + Debug.Assert(semanticTokens.Length == 0 || semanticTokens[0] >= 0, $"Line offset should not be negative."); } return semanticTokens; } - private async Task GetSemanticTokensAsync( - IClientConnection clientConnection, - TextDocumentIdentifier textDocumentIdentifier, - Range range, + private async Task GetSemanticTokensAsync( VersionedDocumentContext documentContext, + LinePositionSpan span, Guid correlationId, bool colorBackground, CancellationToken cancellationToken) @@ -83,12 +72,13 @@ internal class RazorSemanticTokensInfoService( cancellationToken.ThrowIfCancellationRequested(); - var razorSemanticRanges = SemanticTokensVisitor.GetSemanticRanges(codeDocument, range, _razorSemanticTokensLegendService, colorBackground); + var textSpan = span.ToTextSpan(codeDocument.Source.Text); + var razorSemanticRanges = SemanticTokensVisitor.GetSemanticRanges(codeDocument, textSpan, _razorSemanticTokensLegendService, colorBackground); ImmutableArray? csharpSemanticRangesResult = null; try { - csharpSemanticRangesResult = await GetCSharpSemanticRangesAsync(clientConnection, codeDocument, textDocumentIdentifier, range, colorBackground, documentContext.Version, correlationId, cancellationToken).ConfigureAwait(false); + csharpSemanticRangesResult = await GetCSharpSemanticRangesAsync(documentContext, codeDocument, span, colorBackground, correlationId, cancellationToken).ConfigureAwait(false); } catch (OperationCanceledException) { @@ -103,16 +93,13 @@ internal class RazorSemanticTokensInfoService( // We return null (which to the LSP is a no-op) to prevent flashing of CSharp elements. if (csharpSemanticRangesResult is not { } csharpSemanticRanges) { - _logger.LogDebug("Couldn't get C# tokens for version {version} of {doc}. Returning null", documentContext.Version, textDocumentIdentifier.Uri); + _logger.LogDebug("Couldn't get C# tokens for version {version} of {doc}. Returning null", documentContext.Version, documentContext.Uri); return null; } var combinedSemanticRanges = CombineSemanticRanges(razorSemanticRanges, csharpSemanticRanges); - var data = ConvertSemanticRangesToSemanticTokensData(combinedSemanticRanges, codeDocument); - var tokens = new SemanticTokens { Data = data }; - - return tokens; + return ConvertSemanticRangesToSemanticTokensData(combinedSemanticRanges, codeDocument); } private static ImmutableArray CombineSemanticRanges(ImmutableArray razorRanges, ImmutableArray csharpRanges) @@ -149,26 +136,23 @@ private static ImmutableArray CombineSemanticRanges(ImmutableArra return newList.DrainToImmutable(); } - // Internal and virtual for testing only - internal virtual async Task?> GetCSharpSemanticRangesAsync( - IClientConnection clientConnection, + // Virtual for benchmarks + protected virtual async Task?> GetCSharpSemanticRangesAsync( + VersionedDocumentContext documentContext, RazorCodeDocument codeDocument, - TextDocumentIdentifier textDocumentIdentifier, - Range razorRange, + LinePositionSpan razorSpan, bool colorBackground, - long documentVersion, Guid correlationId, - CancellationToken cancellationToken, - string? previousResultId = null) + CancellationToken cancellationToken) { var generatedDocument = codeDocument.GetCSharpDocument(); - Range[]? csharpRanges; + ImmutableArray csharpRanges; // When the feature flag is enabled we try to get a list of precise ranges for the C# code embedded in the Razor document. // The feature flag allows to make calls to Roslyn using multiple smaller and disjoint ranges of the document if (_languageServerFeatureOptions.UsePreciseSemanticTokenRanges) { - if (!TryGetSortedCSharpRanges(codeDocument, razorRange, out csharpRanges)) + if (!TryGetSortedCSharpRanges(codeDocument, razorSpan, out csharpRanges)) { // There's no C# in the range. return ImmutableArray.Empty; @@ -180,17 +164,17 @@ private static ImmutableArray CombineSemanticRanges(ImmutableArra // This single range is the minimal range that contains all of the C# code in the document. // We'll try to call into the mapping service to map to the projected range for us. If that doesn't work, // we'll try to find the minimal range ourselves. - if (!_documentMappingService.TryMapToGeneratedDocumentRange(generatedDocument, razorRange, out var csharpRange) && - !codeDocument.TryGetMinimalCSharpRange(razorRange, out csharpRange)) + if (!_documentMappingService.TryMapToGeneratedDocumentRange(generatedDocument, razorSpan, out var csharpRange) && + !codeDocument.TryGetMinimalCSharpRange(razorSpan, out csharpRange)) { // There's no C# in the range. return ImmutableArray.Empty; } - csharpRanges = new Range[] { csharpRange }; + csharpRanges = [csharpRange]; } - var csharpResponse = await GetMatchingCSharpResponseAsync(clientConnection, textDocumentIdentifier, documentVersion, csharpRanges, correlationId, cancellationToken).ConfigureAwait(false); + var csharpResponse = await _csharpSemanticTokensProvider.GetCSharpSemanticTokensResponseAsync(documentContext, csharpRanges, _languageServerFeatureOptions.UsePreciseSemanticTokenRanges, 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 @@ -220,7 +204,7 @@ private static ImmutableArray CombineSemanticRanges(ImmutableArra var semanticRange = CSharpDataToSemanticRange(lineDelta, charDelta, length, tokenType, tokenModifiers, previousSemanticRange); if (_documentMappingService.TryMapToHostDocumentRange(generatedDocument, semanticRange.AsLinePositionSpan(), out var originalRange)) { - if (razorRange is null || razorRange.ToLinePositionSpan().OverlapsWith(originalRange)) + if (razorSpan.OverlapsWith(originalRange)) { if (colorBackground) { @@ -277,77 +261,10 @@ private static bool ContainsOnlySpacesOrTabs(SourceText razorSource, int startIn return true; } - private async Task GetMatchingCSharpResponseAsync( - IClientConnection clientConnection, - TextDocumentIdentifier textDocumentIdentifier, - long documentVersion, - Range[] csharpRanges, - Guid correlationId, - CancellationToken cancellationToken) - { - var parameter = new ProvideSemanticTokensRangesParams(textDocumentIdentifier, documentVersion, csharpRanges, correlationId); - ProvideSemanticTokensResponse? csharpResponse; - if (_languageServerFeatureOptions.UsePreciseSemanticTokenRanges) - { - 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) - { - var minimalRange = new Range - { - Start = csharpRanges[0].Start, - End = csharpRanges[^1].End - }; - - var newParams = new ProvideSemanticTokensRangesParams( - parameter.TextDocument, - parameter.RequiredHostDocumentVersion, - new[] { minimalRange }, - parameter.CorrelationId); - - csharpResponse = await GetCsharpResponseAsync(clientConnection, newParams, CustomMessageNames.RazorProvideSemanticTokensRangeEndpoint, cancellationToken).ConfigureAwait(false); - } - } - else - { - csharpResponse = await GetCsharpResponseAsync(clientConnection, parameter, CustomMessageNames.RazorProvideSemanticTokensRangeEndpoint, cancellationToken).ConfigureAwait(false); - } - - if (csharpResponse is null) - { - // C# isn't ready yet, don't make Razor wait for it. Once C# is ready they'll send a refresh notification. - return Array.Empty(); - } - - var csharpVersion = csharpResponse.HostDocumentSyncVersion; - if (csharpVersion != documentVersion) - { - // No C# response or C# is out of sync with us. Unrecoverable, return null to indicate no change. - // Once C# syncs up they'll send a refresh notification. - if (csharpVersion == -1) - { - _logger.LogWarning("Didn't get C# tokens because the virtual document wasn't found, or other problem. We were wanting {documentVersion} but C# could not get any version.", documentVersion); - } - else if (csharpVersion < documentVersion) - { - _logger.LogDebug("Didn't wait for Roslyn to get the C# version we were expecting. We are wanting {documentVersion} but C# is at {csharpVersion}.", documentVersion, csharpVersion); - } - else - { - _logger.LogWarning("We are behind the C# version which is surprising. Could be an old request that wasn't cancelled, but if not, expect most future requests to fail. We were wanting {documentVersion} but C# is at {csharpVersion}.", documentVersion, csharpVersion); - } - - return null; - } - - return csharpResponse.Tokens ?? Array.Empty(); - } - // Internal for testing only - internal static bool TryGetSortedCSharpRanges(RazorCodeDocument codeDocument, Range razorRange, [NotNullWhen(true)] out Range[]? ranges) + internal static bool TryGetSortedCSharpRanges(RazorCodeDocument codeDocument, LinePositionSpan razorRange, out ImmutableArray ranges) { - using var _ = ListPool.GetPooledObject(out var csharpRanges); + using var _ = ArrayBuilderPool.GetPooledObject(out var csharpRanges); var csharpSourceText = codeDocument.GetCSharpSourceText(); var sourceText = codeDocument.GetSourceText(); var textSpan = razorRange.ToTextSpan(sourceText); @@ -360,29 +277,32 @@ internal static bool TryGetSortedCSharpRanges(RazorCodeDocument codeDocument, Ra if (textSpan.OverlapsWith(mappedTextSpan)) { - var mappedRange = mapping.GeneratedSpan.ToRange(csharpSourceText); + var mappedRange = mapping.GeneratedSpan.ToLinePositionSpan(csharpSourceText); csharpRanges.Add(mappedRange); } } if (csharpRanges.Count == 0) { - ranges = null; + ranges = []; return false; } - ranges = csharpRanges.ToArray(); - // Ensure the C# ranges are sorted - Array.Sort(ranges, static (r1, r2) => r1.CompareTo(r2)); + csharpRanges.Sort(CompareLinePositionSpans); + ranges = csharpRanges.DrainToImmutable(); return true; } - private Task GetCsharpResponseAsync(IClientConnection clientConnection, ProvideSemanticTokensRangesParams parameter, string lspMethodName, CancellationToken cancellationToken) + private static int CompareLinePositionSpans(LinePositionSpan span1, LinePositionSpan span2) { - return clientConnection.SendRequestAsync( - lspMethodName, - parameter, - cancellationToken); + var result = span1.Start.CompareTo(span2.Start); + + if (result == 0) + { + result = span1.End.CompareTo(span2.End); + } + + return result; } private static SemanticRange CSharpDataToSemanticRange( @@ -428,7 +348,7 @@ private static int[] ConvertSemanticRangesToSemanticTokensData( previousResult = result; } - return data.ToArray(); + return [.. data]; // We purposely capture and manipulate the "data" array here to avoid allocation static void AppendData( diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Semantic/Services/SemanticTokensVisitor.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Semantic/Services/SemanticTokensVisitor.cs index ca1044b967e..a143ad669ce 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Semantic/Services/SemanticTokensVisitor.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Semantic/Services/SemanticTokensVisitor.cs @@ -6,7 +6,6 @@ using System.Linq; using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.Language.Syntax; -using Microsoft.AspNetCore.Razor.LanguageServer.Extensions; using Microsoft.AspNetCore.Razor.PooledObjects; using Microsoft.CodeAnalysis.Razor.Workspaces.Extensions; using Microsoft.CodeAnalysis.Text; @@ -31,14 +30,11 @@ private SemanticTokensVisitor(ImmutableArray.Builder semanticRang _colorCodeBackground = colorCodeBackground; } - public static ImmutableArray GetSemanticRanges(RazorCodeDocument razorCodeDocument, Range range, RazorSemanticTokensLegendService razorSemanticTokensLegendService, bool colorCodeBackground) + public static ImmutableArray GetSemanticRanges(RazorCodeDocument razorCodeDocument, TextSpan textSpan, RazorSemanticTokensLegendService razorSemanticTokensLegendService, bool colorCodeBackground) { - var sourceText = razorCodeDocument.GetSourceText(); - var rangeAsTextSpan = range.ToTextSpan(sourceText); - using var _ = ArrayBuilderPool.GetPooledObject(out var builder); - var visitor = new SemanticTokensVisitor(builder, razorCodeDocument, rangeAsTextSpan, razorSemanticTokensLegendService, colorCodeBackground); + var visitor = new SemanticTokensVisitor(builder, razorCodeDocument, textSpan, razorSemanticTokensLegendService, colorCodeBackground); visitor.Visit(razorCodeDocument.GetSyntaxTree().Root); diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/InlayHints/InlayHintEndpointTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/InlayHints/InlayHintEndpointTest.cs index ac3018ff0e6..327e99ca85e 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/InlayHints/InlayHintEndpointTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/InlayHints/InlayHintEndpointTest.cs @@ -7,7 +7,6 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.LanguageServer.Extensions; -using Microsoft.AspNetCore.Razor.LanguageServer.InlayHints; using Microsoft.AspNetCore.Razor.Test.Common.Workspaces; using Microsoft.CodeAnalysis.Testing; using Microsoft.CodeAnalysis.Text; @@ -16,7 +15,7 @@ using Xunit; using Xunit.Abstractions; -namespace Microsoft.AspNetCore.Razor.LanguageServer.Test.InlayHints; +namespace Microsoft.AspNetCore.Razor.LanguageServer.InlayHints; public class InlayHintEndpointTest(ITestOutputHelper testOutput) : SingleServerDelegatingEndpointTestBase(testOutput) { 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 1202630ed32..2ce440ba156 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 @@ -874,7 +874,7 @@ public void GetMappedCSharpRanges_MinimalRangeVsSmallDisjointRanges_DisjointRang var codeDocument = CreateCodeDocument(documentText, isRazorFile: true, DefaultTagHelpers); var csharpSourceText = codeDocument.GetCSharpSourceText(); - var razorRange = GetRange(documentText); + var razorRange = GetSpan(documentText); if (precise) { @@ -917,11 +917,11 @@ private async Task AssertSemanticTokensAsync( var service = await CreateServiceAsync(documentContext, csharpTokens, withCSharpBackground, serverSupportsPreciseRanges, precise); - var range = GetRange(documentText); - var tokens = await service.GetSemanticTokensAsync(_clientConnection.Object, new() { Uri = documentContext.Uri }, range, documentContext, withCSharpBackground, DisposalToken); + var range = GetSpan(documentText); + var tokens = await service.GetSemanticTokensAsync(documentContext, range, withCSharpBackground, Guid.Empty, DisposalToken); var sourceText = await documentContext.GetSourceTextAsync(DisposalToken); - AssertSemanticTokensMatchesBaseline(sourceText, tokens?.Data, testName.AssumeNotNull()); + AssertSemanticTokensMatchesBaseline(sourceText, tokens, testName.AssumeNotNull()); } private static VersionedDocumentContext CreateDocumentContext( @@ -1006,12 +1006,14 @@ private async Task CreateServiceAsync( options.HtmlVirtualDocumentSuffix == "__virtual.html", MockBehavior.Strict); + var cSharpSemanticTokensProvider = new LSPCSharpSemanticTokensProvider(_clientConnection.Object, LoggerFactory); + var service = new RazorSemanticTokensInfoService( documentMappingService, TestRazorSemanticTokensLegendService.Instance, + cSharpSemanticTokensProvider, featureOptions, - LoggerFactory, - telemetryReporter: null); + LoggerFactory); return service; } @@ -1029,7 +1031,7 @@ private async Task GetCSharpSemanticTokensRespons SpanMappingService, DisposalToken); - var razorRange = GetRange(documentText); + var razorRange = GetSpan(documentText); var csharpRanges = GetMappedCSharpRanges(codeDocument, razorRange, precise); if (csharpRanges == null) { @@ -1040,14 +1042,14 @@ private async Task GetCSharpSemanticTokensRespons { var result = await csharpServer.ExecuteRequestAsync( "roslyn/semanticTokenRanges", - CreateVSSemanticTokensRangesParams(csharpRanges, csharpDocumentUri), + CreateVSSemanticTokensRangesParams(csharpRanges.Value, csharpDocumentUri), DisposalToken); return new ProvideSemanticTokensResponse(tokens: result?.Data, hostDocumentSyncVersion: 0); } else { - var range = Assert.Single(csharpRanges); + var range = Assert.Single(csharpRanges.Value); var result = await csharpServer.ExecuteRequestAsync( "textDocument/semanticTokens/range", CreateVSSemanticTokensRangeParams(range, csharpDocumentUri), @@ -1073,17 +1075,11 @@ private void VerifyTimesLanguageServerCalled(bool serverSupportsPreciseRanges, b !precise || !serverSupportsPreciseRanges ? 1 : 0)); } - private static Range GetRange(string text) + private static LinePositionSpan GetSpan(string text) { var lineCount = text.Count(c => c == '\n') + 1; - var range = new Range - { - Start = new Position { Line = 0, Character = 0 }, - End = new Position { Line = lineCount, Character = 0 } - }; - - return range; + return new LinePositionSpan(new LinePosition(0, 0), new LinePosition(lineCount, 0)); } private void AssertSemanticTokensMatchesBaseline(SourceText sourceText, int[]? actualSemanticTokens, string testName) @@ -1122,7 +1118,7 @@ private string GetBaselineFileContents(string baselineFileName) return baselineContents; } - private Range[]? GetMappedCSharpRanges(RazorCodeDocument codeDocument, Range razorRange, bool precise) + private ImmutableArray? GetMappedCSharpRanges(RazorCodeDocument codeDocument, LinePositionSpan razorRange, bool precise) { var documentMappingService = new RazorDocumentMappingService(FilePathService, new TestDocumentContextFactory(), LoggerFactory); @@ -1147,18 +1143,18 @@ private string GetBaselineFileContents(string baselineFileName) return [range]; } - private static SemanticTokensRangesParams CreateVSSemanticTokensRangesParams(Range[] ranges, Uri uri) + private static SemanticTokensRangesParams CreateVSSemanticTokensRangesParams(ImmutableArray ranges, Uri uri) => new() { TextDocument = new TextDocumentIdentifier { Uri = uri }, - Ranges = ranges + Ranges = ranges.Select(s => s.ToRange()).ToArray() }; - private static SemanticTokensRangeParams CreateVSSemanticTokensRangeParams(Range range, Uri uri) + private static SemanticTokensRangeParams CreateVSSemanticTokensRangeParams(LinePositionSpan range, Uri uri) => new() { TextDocument = new TextDocumentIdentifier { Uri = uri }, - Range = range + Range = range.ToRange() }; private static void GenerateSemanticBaseline(string actualFileContents, string baselineFileName)