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

Add LSP handler to test Gladstone integration #75146

Draft
wants to merge 7 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
using Roslyn.LanguageServer.Protocol;

namespace Microsoft.CodeAnalysis.LanguageServer.Handler.CustomMessage;

internal class CustomMessage(JsonNode message, TextDocumentIdentifier textDocument, Position[] positions)
{
[JsonPropertyName("message")]
public JsonNode Message { get; set; } = Requires.NotNull(message);

[JsonPropertyName("textDocument")]
public TextDocumentIdentifier TextDocument { get; set; } = Requires.NotNull(textDocument);

[JsonPropertyName("positions")]
public Position[] Positions { get; set; } = Requires.NotNull(positions);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System;
using System.Composition;
using System.Linq;
using System.Reflection;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Host.Mef;
using Roslyn.LanguageServer.Protocol;

namespace Microsoft.CodeAnalysis.LanguageServer.Handler.CustomMessage;

[ExportCSharpVisualBasicStatelessLspService(typeof(CustomMessageHandler)), Shared]
[Method(MethodName)]
[method: ImportingConstructor]
[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
internal class CustomMessageHandler()
: ILspServiceDocumentRequestHandler<CustomMessageParams, CustomResponse>
{
private const string MethodName = "roslyn/customMessage";

public bool MutatesSolutionState => false;

public bool RequiresLSPSolution => true;

public TextDocumentIdentifier GetTextDocumentIdentifier(CustomMessageParams request)
{
return request.Message.TextDocument;
}

public async Task<CustomResponse> HandleRequestAsync(CustomMessageParams request, RequestContext context, CancellationToken cancellationToken)
{
// Create the Handler instance. Requires having a parameterless constructor.
// ```
// public class CustomMessageHandler
// {
// public Task<TResponse> ExecuteAsync(TRequest, Document, CancellationToken);
// }
// ```
var handler = Activator.CreateInstanceFrom(request.AssemblyPath, request.TypeFullName).Unwrap();

// Use reflection to find the ExecuteAsync method.
var handlerType = handler.GetType();
var executeMethod = handlerType.GetMethod("ExecuteAsync", BindingFlags.Public | BindingFlags.Instance);

// CustomMessage.Message references positions in CustomMessage.TextDocument as indexes referencing CustomMessage.Positions.
// LinePositionReadConverter allows the deserialization of these indexes into LinePosition objects.
JsonSerializerOptions readOptions = new();
var requestLinePositions = request.Message.Positions.Select(tdp => ProtocolConversions.PositionToLinePosition(tdp)).ToArray();
LinePositionReadConverter linePositionReadConverter = new(requestLinePositions);
readOptions.Converters.Add(linePositionReadConverter);

// Deserialize the message into the expected TRequest type.
var requestType = executeMethod.GetParameters()[0].ParameterType;
var message = JsonSerializer.Deserialize(request.Message.Message, requestType, readOptions);

// Invoke the execute method.
var parameters = new object?[] { message, context.Document, cancellationToken };
var resultTask = (Task)executeMethod.Invoke(handler, parameters);

Check failure on line 65 in src/VisualStudio/Core/Def/LanguageServer/Handler/CustomMessage/CustomMessageHandler.cs

View check run for this annotation

Azure Pipelines / roslyn-CI (Correctness Correctness_Analyzers)

src/VisualStudio/Core/Def/LanguageServer/Handler/CustomMessage/CustomMessageHandler.cs#L65

src/VisualStudio/Core/Def/LanguageServer/Handler/CustomMessage/CustomMessageHandler.cs(65,1): error IDE2000: (NETCORE_ENGINEERING_TELEMETRY=Build) Avoid multiple blank lines (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide2000)

Check failure on line 65 in src/VisualStudio/Core/Def/LanguageServer/Handler/CustomMessage/CustomMessageHandler.cs

View check run for this annotation

Azure Pipelines / roslyn-CI

src/VisualStudio/Core/Def/LanguageServer/Handler/CustomMessage/CustomMessageHandler.cs#L65

src/VisualStudio/Core/Def/LanguageServer/Handler/CustomMessage/CustomMessageHandler.cs(65,1): error IDE2000: (NETCORE_ENGINEERING_TELEMETRY=Build) Avoid multiple blank lines (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide2000)

// Await the result and get its value.
await resultTask.ConfigureAwait(false);
var resultProperty = resultTask.GetType().GetProperty("Result");
var result = resultProperty.GetValue(resultTask);

// CustomResponse.Message must express positions in CustomMessage.TextDocument as indexes referencing CustomResponse.Positions.
// LinePositionWriteConverter allows serializing extender-defined types into json with indexes referencing LinePosition objects.
JsonSerializerOptions writeOptions = new();
LinePositionWriteConverter linePositionWriteConverter = new();
writeOptions.Converters.Add(linePositionWriteConverter);

// Serialize the TResponse and return it to the extension.
var responseType = resultProperty.PropertyType;
var responseJson = JsonSerializer.Serialize(result, responseType, writeOptions);
var responsePositions = linePositionWriteConverter.LinePositions
.OrderBy(p => p.Value)
.Select(p => new Position(p.Key.Line, p.Key.Character))
.ToArray();

return new CustomResponse(JsonNode.Parse(responseJson)!, responsePositions);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System.Text.Json.Serialization;

namespace Microsoft.CodeAnalysis.LanguageServer.Handler.CustomMessage;

internal class CustomMessageParams(string assemblyPath, string typeFullName, CustomMessage message)
{
[JsonPropertyName("assemblyPath")]
public string AssemblyPath { get; } = Requires.NotNull(assemblyPath);

[JsonPropertyName("typeFullName")]
public string TypeFullName { get; } = Requires.NotNull(typeFullName);

[JsonPropertyName("message")]
public CustomMessage Message { get; } = Requires.NotNull(message);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
using Roslyn.LanguageServer.Protocol;

namespace Microsoft.CodeAnalysis.LanguageServer.Handler.CustomMessage;

internal class CustomResponse(JsonNode message, Position[] positions)
{
[JsonPropertyName("message")]
public JsonNode Message { get; } = Requires.NotNull(message);

[JsonPropertyName("positions")]
public Position[] Positions { get; } = Requires.NotNull(positions);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.CodeAnalysis.Text;

namespace Microsoft.CodeAnalysis.LanguageServer.Handler.CustomMessage;

internal class LinePositionReadConverter : JsonConverter<LinePosition>
{
private readonly IReadOnlyList<LinePosition> linePositions;

public LinePositionReadConverter(IReadOnlyList<LinePosition> linePositions)
{
this.linePositions = linePositions;
}

public override LinePosition Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType != JsonTokenType.Number)
{
throw new JsonException();
}

return this.linePositions[reader.GetInt32()];
}

public override void Write(Utf8JsonWriter writer, LinePosition value, JsonSerializerOptions options)
{
throw new NotSupportedException();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.CodeAnalysis.Text;

namespace Microsoft.CodeAnalysis.LanguageServer.Handler.CustomMessage;

internal class LinePositionWriteConverter : JsonConverter<LinePosition>
{
public Dictionary<LinePosition, int> LinePositions { get; } = new();

public override LinePosition Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
throw new NotSupportedException();
}

public override void Write(Utf8JsonWriter writer, LinePosition value, JsonSerializerOptions options)
{
if (!this.LinePositions.TryGetValue(value, out var index))
{
index = this.LinePositions.Count;
this.LinePositions.Add(value, index);
}

writer.WriteNumberValue(index);
}
}
Loading