Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Port semantic tokens range endpoint to cohost server #9761

Merged
merged 12 commits into from
Dec 27, 2023
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
Expand Down Expand Up @@ -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")]
Expand All @@ -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<IClientConnection>();
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)
Expand All @@ -124,28 +121,29 @@ private void EnsureServicesInitialized()
{
var languageServer = RazorLanguageServer.GetInnerLanguageServerForTesting();
RazorSemanticTokenService = languageServer.GetRequiredService<IRazorSemanticTokensInfoService>();
RazorSemanticTokenService.ApplyCapabilities(new(), new VSInternalClientCapabilities { SupportsVisualStudioExtensions = true });
VersionCache = languageServer.GetRequiredService<IDocumentVersionCache>();
ProjectSnapshotManagerDispatcher = languageServer.GetRequiredService<ProjectSnapshotManagerDispatcher>();
}

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<ImmutableArray<SemanticRange>?> GetCSharpSemanticRangesAsync(
IClientConnection clientConnection,
RazorCodeDocument codeDocument,
TextDocumentIdentifier textDocumentIdentifier,
Range razorRange,
RazorSemanticTokensLegend razorSemanticTokensLegend,
bool colorBackground,
long documentVersion,
Guid correlationId,
CancellationToken cancellationToken,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<RazorLSPOptionsMonitor>();
var clientConnection = RazorLanguageServer.GetRequiredService<IClientConnection>();
SemanticTokensRangeEndpoint = new SemanticTokensRangeEndpoint(RazorSemanticTokenService, razorOptionsMonitor, clientConnection);
SemanticTokensRangeEndpoint.ApplyCapabilities(new(), new VSInternalClientCapabilities() { SupportsVisualStudioExtensions = true });

var text = await DocumentContext.GetSourceTextAsync(CancellationToken.None).ConfigureAwait(false);
Expand Down Expand Up @@ -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<ImmutableArray<SemanticRange>?> GetCSharpSemanticRangesAsync(
IClientConnection clientConnection,
RazorCodeDocument codeDocument,
TextDocumentIdentifier textDocumentIdentifier,
Range razorRange,
RazorSemanticTokensLegend razorSemanticTokensLegend,
bool colorBackground,
long documentVersion,
Guid correlationId,
CancellationToken cancellationToken,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<IClientConnection>();

var lineCount = 0;
while (lineCount != documentLineCount)
{
Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,16 +120,19 @@ public static void AddHoverServices(this IServiceCollection services)
services.AddSingleton<IHoverInfoService, HoverInfoService>();
}

public static void AddSemanticTokensServices(this IServiceCollection services)
public static void AddSemanticTokensServices(this IServiceCollection services, LanguageServerFeatureOptions featureOptions)
{
services.AddHandlerWithCapabilities<SemanticTokensRangeEndpoint>();
if (!featureOptions.UseRazorCohostServer)
{
services.AddHandlerWithCapabilities<SemanticTokensRangeEndpoint>();
// Ensure that we don't add the default service if something else has added one.
services.TryAddSingleton<IRazorSemanticTokensInfoService, RazorSemanticTokensInfoService>();
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given that there is no else case for this if (!featureOptions.UseRazorCohostServer) condition,

I wonder where is the cohost setup happening and why we don't add IRazorSemanticTokensInfoService for when UseRazorCohostServer is set to true.

Copy link
Contributor Author

@davidwengier davidwengier Jan 2, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cohosting uses MEF for DI, so the setup is all done via attributes and not by manually adding things to service collections


services.AddHandler<RazorSemanticTokensRefreshEndpoint>();

services.AddSingleton<WorkspaceSemanticTokensRefreshPublisher, DefaultWorkspaceSemanticTokensRefreshPublisher>();
services.AddSingleton<IProjectSnapshotChangeTrigger, DefaultWorkspaceSemanticTokensRefreshTrigger>();

// Ensure that we don't add the default service if something else has added one.
services.TryAddSingleton<IRazorSemanticTokensInfoService, RazorSemanticTokensInfoService>();
}

public static void AddCodeActionsServices(this IServiceCollection services)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
@@ -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<SemanticTokensRangeParams, SemanticTokens?>, ICapabilitiesProvider
[LanguageServerEndpoint(Methods.TextDocumentSemanticTokensRangeName)]
internal sealed class SemanticTokensRangeEndpoint(
IRazorSemanticTokensInfoService semanticTokensInfoService,
RazorLSPOptionsMonitor razorLSPOptionsMonitor,
IClientConnection clientConnection)
: IRazorRequestHandler<SemanticTokensRangeParams, SemanticTokens?>, 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;
Copy link
Contributor

@alexgav alexgav Dec 26, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_semanticTokensInfoService

Why the field instead of using primary constructor parameter in the methods?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See reply above, but TL;DR I like the underscore :)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the underscores too :) Also see my response above. I don't have strong feeling either way, other than - do the primary constructors even make sense then?

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)
Expand All @@ -47,26 +34,10 @@ public TextDocumentIdentifier GetTextDocumentIdentifier(SemanticTokensRangeParam

public async Task<SemanticTokens?> HandleRequestAsync(SemanticTokensRangeParams request, RazorRequestContext requestContext, CancellationToken cancellationToken)
{
if (request is null)
{
throw new ArgumentNullException(nameof(request));
}

var documentContext = requestContext.GetRequiredDocumentContext();
var semanticTokensInfoService = requestContext.GetRequiredService<IRazorSemanticTokensInfoService>();

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;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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<SemanticTokens?> GetSemanticTokensAsync(TextDocumentIdentifier textDocumentIdentifier, Range range, VersionedDocumentContext documentContext, RazorSemanticTokensLegend razorSemanticTokensLegend, Guid correlationId, CancellationToken cancellationToken);
Task<SemanticTokens?> GetSemanticTokensAsync(IClientConnection clientConnection, TextDocumentIdentifier textDocumentIdentifier, Range range, VersionedDocumentContext documentContext, bool colorBackground, CancellationToken cancellationToken);
}
Loading