Skip to content

Commit

Permalink
Switch Roslyn language server to using System.Text.Json
Browse files Browse the repository at this point in the history
  • Loading branch information
dibarbet committed Apr 25, 2024
1 parent 3b7f803 commit ff777c9
Show file tree
Hide file tree
Showing 27 changed files with 417 additions and 157 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
Expand All @@ -19,8 +20,6 @@
using Microsoft.VisualStudio.LanguageServer.Client;
using Microsoft.VisualStudio.Threading;
using Nerdbank.Streams;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Roslyn.LanguageServer.Protocol;
using StreamJsonRpc;

Expand Down Expand Up @@ -200,10 +199,11 @@ public Task OnServerInitializedAsync()
ILspServiceLoggerFactory lspLoggerFactory,
CancellationToken cancellationToken)
{
var jsonMessageFormatter = new JsonMessageFormatter();
VSInternalExtensionUtilities.AddVSInternalExtensionConverters(jsonMessageFormatter.JsonSerializer);
var messageFormatter = new SystemTextJsonFormatter();
messageFormatter.JsonSerializerOptions.AddVSInternalExtensionConverters();
messageFormatter.JsonSerializerOptions.Converters.Add(new NaturalObjectConverter());

var jsonRpc = new JsonRpc(new HeaderDelimitedMessageHandler(outputStream, inputStream, jsonMessageFormatter))
var jsonRpc = new JsonRpc(new HeaderDelimitedMessageHandler(outputStream, inputStream, messageFormatter))
{
ExceptionStrategy = ExceptionProcessing.ISerializable,
};
Expand All @@ -215,7 +215,7 @@ public Task OnServerInitializedAsync()
var hostServices = VisualStudioMefHostServices.Create(_exportProvider);
var server = Create(
jsonRpc,
jsonMessageFormatter.JsonSerializer,
messageFormatter.JsonSerializerOptions,
languageClient,
serverKind,
logger,
Expand All @@ -227,7 +227,7 @@ public Task OnServerInitializedAsync()

public virtual AbstractLanguageServer<RequestContext> Create(
JsonRpc jsonRpc,
JsonSerializer jsonSerializer,
JsonSerializerOptions options,
ICapabilitiesProvider capabilitiesProvider,
WellKnownLspServerKinds serverKind,
AbstractLspLogger logger,
Expand All @@ -236,7 +236,7 @@ public Task OnServerInitializedAsync()
var server = new RoslynLanguageServer(
LspServiceProvider,
jsonRpc,
jsonSerializer,
options,
capabilitiesProvider,
logger,
hostServices,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
using System.Collections.Immutable;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization.Metadata;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
Expand All @@ -26,13 +28,11 @@
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Shared.TestHooks;
using Microsoft.CodeAnalysis.SolutionCrawler;
using Microsoft.CodeAnalysis.Test.Utilities;
using Microsoft.CodeAnalysis.Text;
using Microsoft.CommonLanguageServerProtocol.Framework;
using Nerdbank.Streams;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Roslyn.LanguageServer.Protocol;
using Roslyn.Utilities;
using StreamJsonRpc;
using Xunit;
Expand Down Expand Up @@ -124,8 +124,8 @@ private protected static LSP.ClientCapabilities GetCapabilities(bool isVS)
/// <param name="actual">the actual object to be converted to JSON.</param>
public static void AssertJsonEquals<T1, T2>(T1 expected, T2 actual)
{
var expectedStr = JsonConvert.SerializeObject(expected);
var actualStr = JsonConvert.SerializeObject(actual);
var expectedStr = JsonSerializer.Serialize(expected);
var actualStr = JsonSerializer.Serialize(actual);
AssertEqualIgnoringWhitespace(expectedStr, actualStr);
}

Expand Down Expand Up @@ -269,7 +269,7 @@ private protected static LSP.MarkupContent CreateMarkupContent(LSP.MarkupKind ki
SortText = sortText,
InsertTextFormat = LSP.InsertTextFormat.Plaintext,
Kind = kind,
Data = JObject.FromObject(new CompletionResolveData(resultId, ProtocolConversions.DocumentToTextDocumentIdentifier(document))),
Data = JsonSerializer.SerializeToElement(new CompletionResolveData(resultId, ProtocolConversions.DocumentToTextDocumentIdentifier(document))),
Preselect = preselect,
VsResolveTextEditOnCommit = vsResolveTextEditOnCommit,
LabelDetails = labelDetails
Expand Down Expand Up @@ -512,13 +512,18 @@ private static LSP.DidCloseTextDocumentParams CreateDidCloseTextDocumentParams(U
}
};

internal static JsonMessageFormatter CreateJsonMessageFormatter()
internal static SystemTextJsonFormatter CreateJsonMessageFormatter()
{
var messageFormatter = new JsonMessageFormatter();
LSP.VSInternalExtensionUtilities.AddVSInternalExtensionConverters(messageFormatter.JsonSerializer);
//var messageFormatter = new JsonMessageFormatter();
//LSP.VSInternalExtensionUtilities.AddVSInternalExtensionConverters(messageFormatter.JsonSerializer);
//return messageFormatter;
var messageFormatter = new SystemTextJsonFormatter();
messageFormatter.JsonSerializerOptions.AddVSInternalExtensionConverters();
messageFormatter.JsonSerializerOptions.Converters.Add(new NaturalObjectConverter());
return messageFormatter;
}


internal sealed class TestLspServer : IAsyncDisposable
{
public readonly EditorTestWorkspace TestWorkspace;
Expand Down Expand Up @@ -613,7 +618,7 @@ private static RoslynLanguageServer CreateLanguageServer(Stream inputStream, Str
ExceptionStrategy = ExceptionProcessing.ISerializable,
};

var languageServer = (RoslynLanguageServer)factory.Create(jsonRpc, jsonMessageFormatter.JsonSerializer, capabilitiesProvider, serverKind, logger, workspace.Services.HostServices);
var languageServer = (RoslynLanguageServer)factory.Create(jsonRpc, jsonMessageFormatter.JsonSerializerOptions, capabilitiesProvider, serverKind, logger, workspace.Services.HostServices);

jsonRpc.StartListening();
return languageServer;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using Microsoft.CommonLanguageServerProtocol.Framework;
using Microsoft.Extensions.Logging;
using Microsoft.VisualStudio.Composition;
using Roslyn.LanguageServer.Protocol;
using StreamJsonRpc;

namespace Microsoft.CodeAnalysis.LanguageServer.LanguageServer;
Expand All @@ -28,7 +29,10 @@ internal sealed class LanguageServerHost

public LanguageServerHost(Stream inputStream, Stream outputStream, ExportProvider exportProvider, ILogger logger)
{
var messageFormatter = new JsonMessageFormatter();
var messageFormatter = new SystemTextJsonFormatter();
messageFormatter.JsonSerializerOptions.AddVSInternalExtensionConverters();
messageFormatter.JsonSerializerOptions.Converters.Add(new NaturalObjectConverter());

var handler = new HeaderDelimitedMessageHandler(outputStream, inputStream, messageFormatter);

// If there is a jsonrpc disconnect or server shutdown, that is handled by the AbstractLanguageServer. No need to do anything here.
Expand All @@ -44,7 +48,7 @@ public LanguageServerHost(Stream inputStream, Stream outputStream, ExportProvide
var lspLogger = new LspServiceLogger(_logger);

var hostServices = exportProvider.GetExportedValue<HostServicesProvider>().HostServices;
_roslynLanguageServer = roslynLspFactory.Create(_jsonRpc, messageFormatter.JsonSerializer, capabilitiesProvider, WellKnownLspServerKinds.CSharpVisualBasicLspServer, lspLogger, hostServices);
_roslynLanguageServer = roslynLspFactory.Create(_jsonRpc, messageFormatter.JsonSerializerOptions, capabilitiesProvider, WellKnownLspServerKinds.CSharpVisualBasicLspServer, lspLogger, hostServices);
}

public void Start()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using StreamJsonRpc;

namespace Microsoft.CommonLanguageServerProtocol.Framework;
Expand All @@ -23,8 +21,6 @@ internal abstract class AbstractLanguageServer<TRequestContext>
private readonly JsonRpc _jsonRpc;
protected readonly ILspLogger Logger;

protected readonly JsonSerializer _jsonSerializer;

/// <summary>
/// These are lazy to allow implementations to define custom variables that are used by
/// <see cref="ConstructRequestExecutionQueue"/> or <see cref="ConstructLspServices"/>
Expand Down Expand Up @@ -58,12 +54,10 @@ internal abstract class AbstractLanguageServer<TRequestContext>

protected AbstractLanguageServer(
JsonRpc jsonRpc,
JsonSerializer jsonSerializer,
ILspLogger logger)
{
Logger = logger;
_jsonRpc = jsonRpc;
_jsonSerializer = jsonSerializer;

_jsonRpc.AddLocalRpcTarget(this);
_jsonRpc.Disconnected += JsonRpc_Disconnected;
Expand Down Expand Up @@ -102,7 +96,6 @@ protected virtual AbstractHandlerProvider HandlerProvider

protected virtual void SetupRequestDispatcher(AbstractHandlerProvider handlerProvider)
{
var entryPointMethodInfo = typeof(DelegatingEntryPoint).GetMethod(nameof(DelegatingEntryPoint.ExecuteRequestAsync))!;
// Get unique set of methods from the handler provider for the default language.
foreach (var methodGroup in handlerProvider
.GetRegisteredMethods()
Expand All @@ -127,13 +120,16 @@ protected virtual void SetupRequestDispatcher(AbstractHandlerProvider handlerPro
throw new InvalidOperationException($"Language specific handlers for {methodGroup.Key} have mis-matched number of returns:{Environment.NewLine}{string.Join(Environment.NewLine, methodGroup)}");
}

var delegatingEntryPoint = new DelegatingEntryPoint(methodGroup.Key, this, methodGroup);
var delegatingEntryPoint = CreateDelegatingEntryPoint(methodGroup.Key, methodGroup);
var methodAttribute = new JsonRpcMethodAttribute(methodGroup.Key)
{
UseSingleObjectParameterDeserialization = true,
};

_jsonRpc.AddLocalRpcMethod(entryPointMethodInfo, delegatingEntryPoint, methodAttribute);
// We verified above that parameters match, set flag if this request has parameters or is parameterless so we can set the entrypoint correctly.
var hasParameters = methodGroup.First().RequestType != null;
var entryPoint = delegatingEntryPoint.GetEntryPoint(hasParameters);
_jsonRpc.AddLocalRpcMethod(entryPoint, delegatingEntryPoint, methodAttribute);
}

static bool AllTypesMatch(IEnumerable<Type?> types)
Expand Down Expand Up @@ -178,24 +174,18 @@ protected IRequestExecutionQueue<TRequestContext> GetRequestExecutionQueue()
return _queue.Value;
}

protected virtual string GetLanguageForRequest(string methodName, JToken? parameters)
{
Logger.LogInformation($"Using default language handler for {methodName}");
return LanguageServerConstants.DefaultLanguageName;
}
protected abstract DelegatingEntryPoint CreateDelegatingEntryPoint(string method, IGrouping<string, RequestHandlerMetadata> handlersForMethod);

private sealed class DelegatingEntryPoint
protected abstract class DelegatingEntryPoint
{
private readonly string _method;
private readonly Lazy<FrozenDictionary<string, (MethodInfo MethodInfo, RequestHandlerMetadata Metadata)>> _languageEntryPoint;
private readonly AbstractLanguageServer<TRequestContext> _target;
protected readonly string _method;
protected readonly Lazy<FrozenDictionary<string, (MethodInfo MethodInfo, RequestHandlerMetadata Metadata)>> _languageEntryPoint;

private static readonly MethodInfo s_queueExecuteAsyncMethod = typeof(RequestExecutionQueue<TRequestContext>).GetMethod(nameof(RequestExecutionQueue<TRequestContext>.ExecuteAsync))!;

public DelegatingEntryPoint(string method, AbstractLanguageServer<TRequestContext> target, IGrouping<string, RequestHandlerMetadata> handlersForMethod)
public DelegatingEntryPoint(string method, IGrouping<string, RequestHandlerMetadata> handlersForMethod)
{
_method = method;
_target = target;
_languageEntryPoint = new Lazy<FrozenDictionary<string, (MethodInfo, RequestHandlerMetadata)>>(() =>
{
var handlerEntryPoints = new Dictionary<string, (MethodInfo, RequestHandlerMetadata)>();
Expand All @@ -211,61 +201,40 @@ public DelegatingEntryPoint(string method, AbstractLanguageServer<TRequestContex
});
}

/// <summary>
/// StreamJsonRpc entry point for all handler methods.
/// The optional parameters allow StreamJsonRpc to call into the same method for any kind of request / notification (with any number of params or response).
/// </summary>
public async Task<JToken?> ExecuteRequestAsync(JToken? request = null, CancellationToken cancellationToken = default)
{
var queue = _target.GetRequestExecutionQueue();
var lspServices = _target.GetLspServices();

// Retrieve the language of the request so we know how to deserialize it.
var language = _target.GetLanguageForRequest(_method, request);
public abstract MethodInfo GetEntryPoint(bool hasParameter);

// Find the correct request and response types for the given request and language.
protected (MethodInfo MethodInfo, RequestHandlerMetadata Metadata) GetMethodInfo(string language)
{
if (!_languageEntryPoint.Value.TryGetValue(language, out var requestInfo)
&& !_languageEntryPoint.Value.TryGetValue(LanguageServerConstants.DefaultLanguageName, out requestInfo))
{
throw new InvalidOperationException($"No default or language specific handler was found for {_method} and document with language {language}");
}

// Deserialize the request parameters (if any).
var requestObject = DeserializeRequest(request, requestInfo.Metadata, _target._jsonSerializer);
return requestInfo;
}

var task = requestInfo.MethodInfo.Invoke(queue, [requestObject, _method, language, lspServices, cancellationToken]) as Task
protected async Task<object?> InvokeAsync(
MethodInfo methodInfo,
IRequestExecutionQueue<TRequestContext> queue,
object? requestObject,
string language,
ILspServices lspServices,
CancellationToken cancellationToken)
{
var task = methodInfo.Invoke(queue, [requestObject, _method, language, lspServices, cancellationToken]) as Task
?? throw new InvalidOperationException($"Queue result task cannot be null");
await task.ConfigureAwait(false);
var resultProperty = task.GetType().GetProperty("Result") ?? throw new InvalidOperationException("Result property on task cannot be null");
var result = resultProperty.GetValue(task);
if (result is null || result == NoValue.Instance)
if (result == NoValue.Instance)
{
return null;
}

return JToken.FromObject(result, _target._jsonSerializer);
}

private static object DeserializeRequest(JToken? request, RequestHandlerMetadata metadata, JsonSerializer jsonSerializer)
{
if (request is null && metadata.RequestType is not null)
{
throw new InvalidOperationException($"Handler {metadata.HandlerDescription} requires request parameters but received none");
}

if (request is not null && metadata.RequestType is null)
else
{
throw new InvalidOperationException($"Handler {metadata.HandlerDescription} does not accept parameters, but received some.");
return result;
}

object requestObject = NoValue.Instance;
if (request is not null)
{
requestObject = request.ToObject(metadata.RequestType, jsonSerializer)
?? throw new InvalidOperationException($"Unable to deserialize {request} into {metadata.RequestType} for {metadata.HandlerDescription}");
}

return requestObject;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,12 @@
<Compile Include="$(MSBuildThisFileDirectory)ITextDocumentIdentifierHandler.cs" />
<Compile Include="$(MSBuildThisFileDirectory)LanguageServerConstants.cs" />
<Compile Include="$(MSBuildThisFileDirectory)LanguageServerEndpointAttribute.cs" />
<Compile Include="$(MSBuildThisFileDirectory)NewtonsoftLanguageServer.cs" />
<Compile Include="$(MSBuildThisFileDirectory)QueueItem.cs" />
<Compile Include="$(MSBuildThisFileDirectory)RequestExecutionQueue.cs" />
<Compile Include="$(MSBuildThisFileDirectory)RequestHandlerMetadata.cs" />
<Compile Include="$(MSBuildThisFileDirectory)RequestShutdownEventArgs.cs" />
<Compile Include="$(MSBuildThisFileDirectory)SystemTextJsonLanguageServer.cs" />
<Compile Include="..\..\..\Compilers\Core\Portable\InternalUtilities\IsExternalInit.cs" Link="Utilities\IsExternalInit.cs" />
</ItemGroup>
</Project>

0 comments on commit ff777c9

Please sign in to comment.