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

Adding client capabilities to OOP client initialization data #11129

Merged
merged 12 commits into from
Nov 1, 2024
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 ClientCapabilitiesServiceBase : IClientCapabilitiesService
alexgav marked this conversation as resolved.
Show resolved Hide resolved
{
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 _remoteClientCapabilitiesService = args.ExportProvider.GetExportedValue<IClientCapabilitiesService>();
DustinCampbell marked this conversation as resolved.
Show resolved Hide resolved

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: _remoteClientCapabilitiesService.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 : ClientCapabilitiesServiceBase;
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
@@ -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 : ClientCapabilitiesServiceBase;
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,6 +45,13 @@ public void JsonServicesHaveTheRightParameters(Type serviceType, Type? _)
{
Assert.True(typeof(IRemoteJsonService).IsAssignableFrom(serviceType));

if (serviceType == typeof(IRemoteClientInitializationService))
{
// IRemoteClientInitializationService is a special init service that doesn't follow naming or parameter type
// conventions of other remote JSON services
return;
}

alexgav marked this conversation as resolved.
Show resolved Hide resolved
var found = false;
foreach (var method in serviceType.GetMethods(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public))
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
using Microsoft.CodeAnalysis.Remote.Razor.ProjectSystem;
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 +30,13 @@ 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>();

/// <summary>
/// The export provider for Razor OOP services (not Roslyn)
Expand Down Expand Up @@ -64,6 +67,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 +87,12 @@ private protected void UpdateClientInitializationOptions(Func<RemoteClientInitia
FeatureOptions.SetOptions(_clientInitializationOptions);
}

private protected void UpdateClientLSPInitializationOptions(Func<RemoteClientLSPInitializationOptions, RemoteClientLSPInitializationOptions> mutation)
alexgav marked this conversation as resolved.
Show resolved Hide resolved
{
_clientLSPInitializationOptions = mutation(_clientLSPInitializationOptions);
ClientCapabilities.SetCapabilities(_clientLSPInitializationOptions.ClientCapabilities);
}

protected Task<TextDocument> CreateProjectAndRazorDocumentAsync(
string contents,
string? fileKind = null,
Expand Down