Skip to content

Commit

Permalink
Adding client capabilities to OOP client initialization data (#11129)
Browse files Browse the repository at this point in the history
* Add RemoteClientCapabilities service

The service gets initialized once during initial connection / init and provides client capabilities to other remote services.

* Add RemoteClientCapabilitiesService
- Adding client capabilities to RemoteClientLSPInitializationOptions
- Converting IRemoteClientIntializationService to be a JSON service for simplicity of data serialization
- Converting client initialization code to use JSON client to call IRemoteClientIntializationService

* Fixing tests

* Removing unneeded class

* Fixing tests

* Export IClientCapabilitiesService and consume it when appropriate

Only consumers that are initializing capabiilities service by calling SetCapabilities should be importing it via RemoteClientCapabilitiesService. All other consumers should be using IClientCapabilitiesService.

* Simplifying code, moving and correcting comment

* Test fixup per PR feedback

* Service rename per PR feedback

* More PR feedback

- Renaming a variable
- Made UpdateClientLSPInitializationOptions mode complete (so it updates RemoteSemanticTokensLegendService now with initialization data)
- Removed limitation of single update on RemoteSemanticTokensLegendService
- Use UpdateClientLSPInitializationOptions  in cohost semantic tokens test

* Removed unused service
  • Loading branch information
alexgav authored Nov 1, 2024
1 parent 09cba53 commit 6ff6566
Show file tree
Hide file tree
Showing 14 changed files with 111 additions and 70 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT license. See License.txt in the project root for license information.

using System;
using Microsoft.VisualStudio.LanguageServer.Protocol;

namespace Microsoft.CodeAnalysis.Razor.Protocol;

internal abstract class AbstractClientCapabilitiesService : IClientCapabilitiesService
{
private VSInternalClientCapabilities? _clientCapabilities;

public bool CanGetClientCapabilities => _clientCapabilities is not null;

public VSInternalClientCapabilities ClientCapabilities => _clientCapabilities ?? throw new InvalidOperationException("Client capabilities requested before initialized.");

public void SetCapabilities(VSInternalClientCapabilities clientCapabilities)
{
_clientCapabilities = clientCapabilities;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

namespace Microsoft.CodeAnalysis.Razor.Remote;

internal interface IRemoteClientInitializationService
internal interface IRemoteClientInitializationService : IRemoteJsonService
{
ValueTask InitializeAsync(RemoteClientInitializationOptions initializationOptions, CancellationToken cancellationToken);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ internal static class RazorServices
[
(typeof(IRemoteLinkedEditingRangeService), null),
(typeof(IRemoteTagHelperProviderService), null),
(typeof(IRemoteClientInitializationService), null),
(typeof(IRemoteSemanticTokensService), null),
(typeof(IRemoteHtmlDocumentService), null),
(typeof(IRemoteUriPresentationService), null),
Expand All @@ -29,6 +28,7 @@ internal static class RazorServices
// Internal for testing
internal static readonly IEnumerable<(Type, Type?)> JsonServices =
[
(typeof(IRemoteClientInitializationService), null),
(typeof(IRemoteGoToDefinitionService), null),
(typeof(IRemoteSignatureHelpService), null),
(typeof(IRemoteInlayHintService), null),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,31 +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.Runtime.Serialization;
using System.Text.Json.Serialization;

namespace Microsoft.CodeAnalysis.Razor.Remote;

[DataContract]
internal struct RemoteClientInitializationOptions
{
[DataMember(Order = 0)]
internal required bool UseRazorCohostServer;
[JsonPropertyName("useRazorCohostServer")]
public required bool UseRazorCohostServer { get; set; }

[DataMember(Order = 1)]
internal required bool UsePreciseSemanticTokenRanges;
[JsonPropertyName("usePreciseSemanticTokenRanges")]
public required bool UsePreciseSemanticTokenRanges { get; set; }

[DataMember(Order = 2)]
internal required string CSharpVirtualDocumentSuffix;
[JsonPropertyName("csharpVirtualDocumentSuffix")]
public required string CSharpVirtualDocumentSuffix { get; set; }

[DataMember(Order = 3)]
internal required string HtmlVirtualDocumentSuffix;
[JsonPropertyName("htmlVirtualDocumentSuffix")]
public required string HtmlVirtualDocumentSuffix { get; set; }

[DataMember(Order = 4)]
internal required bool IncludeProjectKeyInGeneratedFilePath;
[JsonPropertyName("includeProjectKeyInGeneratedFilePath")]
public required bool IncludeProjectKeyInGeneratedFilePath { get; set; }

[DataMember(Order = 5)]
internal required bool ReturnCodeActionAndRenamePathsWithPrefixedSlash;
[JsonPropertyName("returnCodeActionAndRenamePathsWithPrefixedSlash")]
public required bool ReturnCodeActionAndRenamePathsWithPrefixedSlash { get; set; }

[DataMember(Order = 6)]
internal required bool ForceRuntimeCodeGeneration;
[JsonPropertyName("forceRuntimeCodeGeneration")]
public required bool ForceRuntimeCodeGeneration { get; set; }
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT license. See License.txt in the project root for license information.

using System.Runtime.Serialization;
using System.Text.Json.Serialization;
using Microsoft.VisualStudio.LanguageServer.Protocol;

namespace Microsoft.CodeAnalysis.Razor.Remote;

[DataContract]
internal struct RemoteClientLSPInitializationOptions
{
[DataMember(Order = 0)]
internal required string[] TokenTypes;
[JsonPropertyName("tokenTypes")]
public required string[] TokenTypes { get; set; }

[DataMember(Order = 1)]
internal required string[] TokenModifiers;
[JsonPropertyName("tokenModifiers")]
public required string[] TokenModifiers { get; set; }

[JsonPropertyName("clientCapabilities")]
public required VSInternalClientCapabilities ClientCapabilities { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.CodeAnalysis.ExternalAccess.Razor;
using Microsoft.CodeAnalysis.Razor.Protocol;
using Microsoft.CodeAnalysis.Razor.Protocol.DocumentSymbols;
using Microsoft.CodeAnalysis.Razor.Remote;
using Microsoft.CodeAnalysis.Remote.Razor.ProjectSystem;
Expand All @@ -23,6 +24,7 @@ protected override IRemoteDocumentSymbolService CreateService(in ServiceArgs arg
}

private readonly IDocumentSymbolService _documentSymbolService = args.ExportProvider.GetExportedValue<IDocumentSymbolService>();
private readonly IClientCapabilitiesService _clientCapabilitiesService = args.ExportProvider.GetExportedValue<IClientCapabilitiesService>();

public ValueTask<SumType<DocumentSymbol[], SymbolInformation[]>?> GetDocumentSymbolsAsync(JsonSerializableRazorPinnedSolutionInfoWrapper solutionInfo, JsonSerializableDocumentId razorDocumentId, bool useHierarchicalSymbols, CancellationToken cancellationToken)
=> RunServiceAsync(
Expand All @@ -40,8 +42,7 @@ protected override IRemoteDocumentSymbolService CreateService(in ServiceArgs arg
var csharpSymbols = await ExternalHandlers.DocumentSymbols.GetDocumentSymbolsAsync(
generatedDocument,
useHierarchicalSymbols,
// TODO: use correct value from client capabilities when https://github.com/dotnet/razor/issues/11102
supportsVSExtensions: true,
supportsVSExtensions: _clientCapabilitiesService.ClientCapabilities.SupportsVisualStudioExtensions,
cancellationToken).ConfigureAwait(false);

var codeDocument = await context.GetCodeDocumentAsync(cancellationToken).ConfigureAwait(false);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// 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 Microsoft.CodeAnalysis.Razor.Protocol;

namespace Microsoft.CodeAnalysis.Remote.Razor;

[Shared]
[Export(typeof(IClientCapabilitiesService))]
[Export(typeof(RemoteClientCapabilitiesService))]
internal sealed class RemoteClientCapabilitiesService : AbstractClientCapabilitiesService;
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ protected override IRemoteClientInitializationService CreateService(in ServiceAr
=> new RemoteClientInitializationService(in args);
}

private readonly RemoteClientCapabilitiesService _remoteClientCapabilitiesService = args.ExportProvider.GetExportedValue<RemoteClientCapabilitiesService>();
private readonly RemoteLanguageServerFeatureOptions _remoteLanguageServerFeatureOptions = args.ExportProvider.GetExportedValue<RemoteLanguageServerFeatureOptions>();
private readonly RemoteSemanticTokensLegendService _remoteSemanticTokensLegendService = args.ExportProvider.GetExportedValue<RemoteSemanticTokensLegendService>();

Expand All @@ -31,6 +32,7 @@ public ValueTask InitializeLSPAsync(RemoteClientLSPInitializationOptions options
=> RunServiceAsync(ct =>
{
_remoteSemanticTokensLegendService.SetLegend(options.TokenTypes, options.TokenModifiers);
_remoteClientCapabilitiesService.SetCapabilities(options.ClientCapabilities);
return default;
},
cancellationToken);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,7 @@ internal sealed class RemoteSemanticTokensLegendService : ISemanticTokensLegendS

public void SetLegend(string[] tokenTypes, string[] tokenModifiers)
{
if (_tokenTypes is null)
{
_tokenTypes = new SemanticTokenTypes(tokenTypes);
_tokenModifiers = new SemanticTokenModifiers(tokenModifiers);
}
_tokenTypes = new SemanticTokenTypes(tokenTypes);
_tokenModifiers = new SemanticTokenModifiers(tokenModifiers);
}
}
Original file line number Diff line number Diff line change
@@ -1,25 +1,11 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT license. See License.txt in the project root for license information.

using System;
using System.ComponentModel.Composition;
using Microsoft.CodeAnalysis.Razor.Protocol;
using Microsoft.VisualStudio.LanguageServer.Protocol;

namespace Microsoft.VisualStudio.Razor.LanguageClient.Cohost;

[Export(typeof(RazorCohostClientCapabilitiesService))]
[Export(typeof(IClientCapabilitiesService))]
internal class RazorCohostClientCapabilitiesService : IClientCapabilitiesService
{
private VSInternalClientCapabilities? _clientCapabilities;

public bool CanGetClientCapabilities => _clientCapabilities is not null;

public VSInternalClientCapabilities ClientCapabilities => _clientCapabilities ?? throw new InvalidOperationException("Client capabilities requested before initialized.");

public void SetCapabilities(VSInternalClientCapabilities clientCapabilities)
{
_clientCapabilities = clientCapabilities;
}
}
internal sealed class RazorCohostClientCapabilitiesService : AbstractClientCapabilitiesService;
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,13 @@ internal sealed class RemoteServiceInvoker(

private async Task<RazorRemoteHostClient?> TryGetClientAsync(CancellationToken cancellationToken)
{
// Even if we're getting a service that wants to use MessagePack, we still have to initialize the OOP client
// so we get the JSON client too and use it to call initialization service
if (!_fullyInitialized)
{
_ = await TryGetJsonClientAsync(cancellationToken).ConfigureAwait(false);
}

var workspace = _workspaceProvider.GetWorkspace();

var remoteClient = await RazorRemoteHostClient.TryGetClientAsync(
Expand All @@ -89,32 +96,27 @@ internal sealed class RemoteServiceInvoker(
RazorRemoteServiceCallbackDispatcherRegistry.Empty,
cancellationToken).ConfigureAwait(false);

if (remoteClient is null)
{
return null;
}

await InitializeRemoteClientAsync(remoteClient, cancellationToken).ConfigureAwait(false);

return remoteClient;
}

private async Task<RazorRemoteHostClient?> TryGetJsonClientAsync(CancellationToken cancellationToken)
{
// Even if we're getting a service that wants to use Json, we still have to initialize the OOP client
// so we get the regular (MessagePack) client too.
if (!_fullyInitialized)
{
_ = await TryGetClientAsync(cancellationToken).ConfigureAwait(false);
}

var workspace = _workspaceProvider.GetWorkspace();

return await RazorRemoteHostClient.TryGetClientAsync(
var remoteClient = await RazorRemoteHostClient.TryGetClientAsync(
workspace.Services,
RazorServices.JsonDescriptors,
RazorRemoteServiceCallbackDispatcherRegistry.Empty,
cancellationToken).ConfigureAwait(false);

if (remoteClient is null)
{
return null;
}

await InitializeRemoteClientAsync(remoteClient, cancellationToken).ConfigureAwait(false);

return remoteClient;
}

private async Task InitializeRemoteClientAsync(RazorRemoteHostClient remoteClient, CancellationToken cancellationToken)
Expand Down Expand Up @@ -157,6 +159,7 @@ private async Task InitializeRemoteClientAsync(RazorRemoteHostClient remoteClien
{
var initParams = new RemoteClientLSPInitializationOptions
{
ClientCapabilities = _clientCapabilitiesService.ClientCapabilities,
TokenTypes = _semanticTokensLegendService.TokenTypes.All,
TokenModifiers = _semanticTokensLegendService.TokenModifiers.All,
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ public void JsonServicesHaveTheRightParameters(Type serviceType, Type? _)
{
Assert.True(typeof(IRemoteJsonService).IsAssignableFrom(serviceType));

var found = false;
foreach (var method in serviceType.GetMethods(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public))
{
if (method.Name != "RunServiceAsync" &&
Expand All @@ -55,14 +54,9 @@ public void JsonServicesHaveTheRightParameters(Type serviceType, Type? _)
{
Assert.Fail($"Method {method.Name} in a Json service has a pinned solution info wrapper parameter that isn't Json serializable");
}
else if (typeof(JsonSerializableRazorPinnedSolutionInfoWrapper).IsAssignableFrom(parameterType))
{
found = true;
}

}
}

Assert.True(found, "Didn't find a method to validate, which means maybe this test is invalid");
}

[Fact]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@
using Microsoft.CodeAnalysis.Razor.Workspaces;
using Microsoft.CodeAnalysis.Remote.Razor;
using Microsoft.CodeAnalysis.Remote.Razor.ProjectSystem;
using Microsoft.CodeAnalysis.Remote.Razor.SemanticTokens;
using Microsoft.CodeAnalysis.Text;
using Microsoft.VisualStudio.Composition;
using Microsoft.VisualStudio.LanguageServer.Protocol;
using Xunit.Abstractions;

namespace Microsoft.VisualStudio.Razor.LanguageClient.Cohost;
Expand All @@ -29,11 +31,14 @@ public abstract class CohostEndpointTestBase(ITestOutputHelper testOutputHelper)
private ExportProvider? _exportProvider;
private TestRemoteServiceInvoker? _remoteServiceInvoker;
private RemoteClientInitializationOptions _clientInitializationOptions;
private RemoteClientLSPInitializationOptions _clientLSPInitializationOptions;
private IFilePathService? _filePathService;

private protected TestRemoteServiceInvoker RemoteServiceInvoker => _remoteServiceInvoker.AssumeNotNull();
private protected IFilePathService FilePathService => _filePathService.AssumeNotNull();
private protected RemoteLanguageServerFeatureOptions FeatureOptions => OOPExportProvider.GetExportedValue<RemoteLanguageServerFeatureOptions>();
private protected RemoteClientCapabilitiesService ClientCapabilities => OOPExportProvider.GetExportedValue<RemoteClientCapabilitiesService>();
private protected RemoteSemanticTokensLegendService SemanticTokensLegendService => OOPExportProvider.GetExportedValue<RemoteSemanticTokensLegendService>();

/// <summary>
/// The export provider for Razor OOP services (not Roslyn)
Expand Down Expand Up @@ -64,6 +69,17 @@ protected override async Task InitializeAsync()
};
UpdateClientInitializationOptions(c => c);

_clientLSPInitializationOptions = new()
{
ClientCapabilities = new VSInternalClientCapabilities()
{
SupportsVisualStudioExtensions = true
},
TokenTypes = [],
TokenModifiers = []
};
UpdateClientLSPInitializationOptions(c => c);

_filePathService = new RemoteFilePathService(FeatureOptions);
}

Expand All @@ -73,6 +89,13 @@ private protected void UpdateClientInitializationOptions(Func<RemoteClientInitia
FeatureOptions.SetOptions(_clientInitializationOptions);
}

private protected void UpdateClientLSPInitializationOptions(Func<RemoteClientLSPInitializationOptions, RemoteClientLSPInitializationOptions> mutation)
{
_clientLSPInitializationOptions = mutation(_clientLSPInitializationOptions);
ClientCapabilities.SetCapabilities(_clientLSPInitializationOptions.ClientCapabilities);
SemanticTokensLegendService.SetLegend(_clientLSPInitializationOptions.TokenTypes, _clientLSPInitializationOptions.TokenModifiers);
}

protected Task<TextDocument> CreateProjectAndRazorDocumentAsync(
string contents,
string? fileKind = null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using Microsoft.AspNetCore.Razor.LanguageServer.Semantic;
using Microsoft.AspNetCore.Razor.PooledObjects;
using Microsoft.AspNetCore.Razor.Telemetry;
using Microsoft.CodeAnalysis.Razor.SemanticTokens;
using Microsoft.CodeAnalysis.Razor.Settings;
using Microsoft.CodeAnalysis.Remote.Razor.SemanticTokens;
using Microsoft.CodeAnalysis.Text;
Expand Down Expand Up @@ -97,8 +98,7 @@ private async Task VerifySemanticTokensAsync(string input, bool colorBackground,
var legend = TestRazorSemanticTokensLegendService.Instance;

// We need to manually initialize the OOP service so we can get semantic token info later
var legendService = OOPExportProvider.GetExportedValue<RemoteSemanticTokensLegendService>();
legendService.SetLegend(legend.TokenTypes.All, legend.TokenModifiers.All);
UpdateClientLSPInitializationOptions(options => options with { TokenTypes = legend.TokenTypes.All, TokenModifiers = legend.TokenModifiers.All });

// Update the client initialization options to control the precise ranges option
UpdateClientInitializationOptions(c => c with { UsePreciseSemanticTokenRanges = precise });
Expand Down

0 comments on commit 6ff6566

Please sign in to comment.