From 52fae51eb51e22929567206063a6a15daa02bea0 Mon Sep 17 00:00:00 2001 From: David Driscoll Date: Wed, 12 Aug 2020 22:05:42 -0400 Subject: [PATCH 1/2] Added support for class based handlers to HandlerTypeDescriptorHelper/LspHandlerTypeDescriptorHelper. Added unit test demonstrating a custom params / handler pair for Execute Command Made the OptionalAttribute's public Added IExecuteCommandParams to allow for custom command execution handlers to work correctly. --- .../LanguageClientRegistrationManager.cs | 20 +-- src/Dap.Protocol/Requests/IAttachHandler.cs | 2 +- src/Dap.Protocol/Requests/ILaunchHandler.cs | 2 +- .../Serialization/OptionalAttribute.cs | 2 +- .../DebugAdapterHandlerCollection.cs | 2 +- src/JsonRpc/HandlerTypeDescriptor.cs | 45 ++++- src/JsonRpc/HandlerTypeDescriptorHelper.cs | 33 ++-- src/JsonRpc/InputHandler.cs | 161 +++++++++--------- src/Protocol/Models/ExecuteCommandParams.cs | 2 +- src/Protocol/Models/IExecuteCommandParams.cs | 18 ++ .../Serialization/OptionalAttribute.cs | 2 +- .../Shared/LspHandlerTypeDescriptorHelper.cs | 32 ++-- src/Server/Matchers/ExecuteCommandMatcher.cs | 2 +- src/Shared/LspRequestRouter.cs | 2 +- src/Shared/SharedHandlerCollection.cs | 2 +- .../Integration/OverrideHandlerTests.cs | 112 ++++++++++++ test/Lsp.Tests/Lsp.Tests.csproj | 5 +- 17 files changed, 303 insertions(+), 141 deletions(-) create mode 100644 src/Protocol/Models/IExecuteCommandParams.cs create mode 100644 test/Lsp.Tests/Integration/OverrideHandlerTests.cs diff --git a/src/Client/LanguageClientRegistrationManager.cs b/src/Client/LanguageClientRegistrationManager.cs index 03399ff75..c798044d7 100644 --- a/src/Client/LanguageClientRegistrationManager.cs +++ b/src/Client/LanguageClientRegistrationManager.cs @@ -65,16 +65,16 @@ public void RegisterCapabilities(ServerCapabilities serverCapabilities) serverCapabilities )) { - var descriptor = LspHandlerTypeDescriptorHelper.GetHandlerTypeForRegistrationOptions(registrationOptions); - if (descriptor == null) + var method = LspHandlerTypeDescriptorHelper.GetMethodForRegistrationOptions(registrationOptions); + if (method == null) { - _logger.LogWarning("Unable to find handler type descriptor for the given {@RegistrationOptions}", registrationOptions); + _logger.LogWarning("Unable to find method for given {@RegistrationOptions}", registrationOptions); continue; } var reg = new Registration { Id = registrationOptions.Id, - Method = descriptor.Method, + Method = method, RegisterOptions = registrationOptions }; _registrations.AddOrUpdate(registrationOptions.Id, x => reg, (a, b) => reg); @@ -91,8 +91,8 @@ public void RegisterCapabilities(ServerCapabilities serverCapabilities) .Workspace )) { - var descriptor = LspHandlerTypeDescriptorHelper.GetHandlerTypeForRegistrationOptions(registrationOptions); - if (descriptor == null) + var method = LspHandlerTypeDescriptorHelper.GetMethodForRegistrationOptions(registrationOptions); + if (method == null) { // TODO: Log this continue; @@ -100,7 +100,7 @@ public void RegisterCapabilities(ServerCapabilities serverCapabilities) var reg = new Registration { Id = registrationOptions.Id, - Method = descriptor.Method, + Method = method, RegisterOptions = registrationOptions }; _registrations.AddOrUpdate(registrationOptions.Id, x => reg, (a, b) => reg); @@ -117,8 +117,8 @@ private void Register(params Registration[] registrations) private void Register(Registration registration) { - var typeDescriptor = LspHandlerTypeDescriptorHelper.GetHandlerTypeDescriptor(registration.Method); - if (typeDescriptor == null) + var registrationType = LspHandlerTypeDescriptorHelper.GetRegistrationType(registration.Method); + if (registrationType == null) { _registrations.AddOrUpdate(registration.Id, x => registration, (a, b) => registration); return; @@ -128,7 +128,7 @@ private void Register(Registration registration) Id = registration.Id, Method = registration.Method, RegisterOptions = registration.RegisterOptions is JToken token - ? token.ToObject(typeDescriptor.RegistrationType, _serializer.JsonSerializer) + ? token.ToObject(registrationType, _serializer.JsonSerializer) : registration.RegisterOptions }; _registrations.AddOrUpdate(deserializedRegistration.Id, x => deserializedRegistration, (a, b) => deserializedRegistration); diff --git a/src/Dap.Protocol/Requests/IAttachHandler.cs b/src/Dap.Protocol/Requests/IAttachHandler.cs index 539afbf07..07df0dba2 100644 --- a/src/Dap.Protocol/Requests/IAttachHandler.cs +++ b/src/Dap.Protocol/Requests/IAttachHandler.cs @@ -22,7 +22,7 @@ public abstract class AttachHandlerBase : IAttachHandler where T : AttachR public abstract Task Handle(T request, CancellationToken cancellationToken); } - public abstract class AttachHandler : AttachHandlerBase + public abstract class AttachHandler : AttachHandlerBase, IAttachHandler { } } diff --git a/src/Dap.Protocol/Requests/ILaunchHandler.cs b/src/Dap.Protocol/Requests/ILaunchHandler.cs index 98a0afe0e..9d5dd861f 100644 --- a/src/Dap.Protocol/Requests/ILaunchHandler.cs +++ b/src/Dap.Protocol/Requests/ILaunchHandler.cs @@ -22,7 +22,7 @@ public abstract class LaunchHandlerBase : ILaunchHandler where T : LaunchR public abstract Task Handle(T request, CancellationToken cancellationToken); } - public abstract class LaunchHandler : LaunchHandlerBase + public abstract class LaunchHandler : LaunchHandlerBase, ILaunchHandler { } } diff --git a/src/Dap.Protocol/Serialization/OptionalAttribute.cs b/src/Dap.Protocol/Serialization/OptionalAttribute.cs index 800fb25a0..763b5f806 100644 --- a/src/Dap.Protocol/Serialization/OptionalAttribute.cs +++ b/src/Dap.Protocol/Serialization/OptionalAttribute.cs @@ -3,7 +3,7 @@ namespace OmniSharp.Extensions.DebugAdapter.Protocol.Serialization { [AttributeUsage(AttributeTargets.Property)] - internal class OptionalAttribute : Attribute + public class OptionalAttribute : Attribute { } } diff --git a/src/Dap.Shared/DebugAdapterHandlerCollection.cs b/src/Dap.Shared/DebugAdapterHandlerCollection.cs index 6a8fb2185..7148360ca 100644 --- a/src/Dap.Shared/DebugAdapterHandlerCollection.cs +++ b/src/Dap.Shared/DebugAdapterHandlerCollection.cs @@ -122,7 +122,7 @@ private CompositeDisposable AddHandler(IJsonRpcHandler handler, JsonRpcHandlerOp private HandlerDescriptor GetDescriptor(string method, Type handlerType, IJsonRpcHandler handler, JsonRpcHandlerOptions options) { - var typeDescriptor = HandlerTypeDescriptorHelper.GetHandlerTypeDescriptor(method); + var typeDescriptor = HandlerTypeDescriptorHelper.GetHandlerTypeDescriptor(handlerType); var @interface = HandlerTypeDescriptorHelper.GetHandlerInterface(handlerType); return GetDescriptor(method, handlerType, handler, options, typeDescriptor, @interface); diff --git a/src/JsonRpc/HandlerTypeDescriptor.cs b/src/JsonRpc/HandlerTypeDescriptor.cs index 62e0c9a57..a0fb9dd0f 100644 --- a/src/JsonRpc/HandlerTypeDescriptor.cs +++ b/src/JsonRpc/HandlerTypeDescriptor.cs @@ -6,17 +6,22 @@ namespace OmniSharp.Extensions.JsonRpc { - [DebuggerDisplay("{" + nameof(Method) + "}")] - internal class HandlerTypeDescriptor : IHandlerTypeDescriptor + [DebuggerDisplay("{ToString()}")] + internal class HandlerTypeDescriptor : IHandlerTypeDescriptor, IEquatable { public HandlerTypeDescriptor(Type handlerType) { var method = MethodAttribute.From(handlerType); Method = method.Method; Direction = method.Direction; - if (handlerType.IsGenericTypeDefinition) + if (handlerType.IsGenericTypeDefinition && handlerType.IsPublic) { - handlerType = handlerType.MakeGenericType(handlerType.GetTypeInfo().GenericTypeParameters[0].GetGenericParameterConstraints()[0]); + var parameter = handlerType.GetTypeInfo().GenericTypeParameters[0]; + var constraints = parameter.GetGenericParameterConstraints(); + if (constraints.Length == 1) + { + handlerType = handlerType.MakeGenericType(handlerType.GetTypeInfo().GenericTypeParameters[0].GetGenericParameterConstraints()[0]); + } } HandlerType = handlerType; @@ -74,6 +79,36 @@ public HandlerTypeDescriptor(Type handlerType) public Type ParamsType { get; } public bool HasResponseType { get; } public Type ResponseType { get; } - public override string ToString() => $"{Method}"; + public override string ToString() => $"{Method}:{HandlerType.FullName}"; + + public bool Equals(HandlerTypeDescriptor other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return Method == other.Method && HandlerType.Equals(other.HandlerType) && InterfaceType.Equals(other.InterfaceType); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != this.GetType()) return false; + return Equals((HandlerTypeDescriptor) obj); + } + + public override int GetHashCode() + { + unchecked + { + var hashCode = Method.GetHashCode(); + hashCode = ( hashCode * 397 ) ^ HandlerType.GetHashCode(); + hashCode = ( hashCode * 397 ) ^ InterfaceType.GetHashCode(); + return hashCode; + } + } + + public static bool operator ==(HandlerTypeDescriptor left, HandlerTypeDescriptor right) => Equals(left, right); + + public static bool operator !=(HandlerTypeDescriptor left, HandlerTypeDescriptor right) => !Equals(left, right); } } diff --git a/src/JsonRpc/HandlerTypeDescriptorHelper.cs b/src/JsonRpc/HandlerTypeDescriptorHelper.cs index 778868c24..4ec3a65b3 100644 --- a/src/JsonRpc/HandlerTypeDescriptorHelper.cs +++ b/src/JsonRpc/HandlerTypeDescriptorHelper.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Concurrent; +using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using System.Reflection; @@ -12,7 +13,7 @@ public static class HandlerTypeDescriptorHelper private static readonly ConcurrentDictionary MethodNames = new ConcurrentDictionary(); - internal static readonly ImmutableSortedDictionary KnownHandlers; + internal static readonly ILookup KnownHandlers; static HandlerTypeDescriptorHelper() { @@ -31,14 +32,14 @@ static HandlerTypeDescriptorHelper() } } ) - .Where(z => z.IsInterface && typeof(IJsonRpcHandler).IsAssignableFrom(z)) + .Where(z => (z.IsInterface || (z.IsClass && !z.IsAbstract)) && typeof(IJsonRpcHandler).IsAssignableFrom(z)) .Where(z => MethodAttribute.From(z) != null) .Where(z => !z.Name.EndsWith("Manager")) // Manager interfaces are generally specializations around the handlers .Select(GetMethodType) .Distinct() .ToLookup(x => MethodAttribute.From(x).Method) - .Select(x => new HandlerTypeDescriptor(x.First()) as IHandlerTypeDescriptor) - .ToImmutableSortedDictionary(x => x.Method, x => x, StringComparer.Ordinal); + .SelectMany(x => x.Select(z => new HandlerTypeDescriptor(z) as IHandlerTypeDescriptor)) + .ToLookup(x => x.Method, StringComparer.Ordinal); } catch (Exception e) { @@ -46,28 +47,23 @@ static HandlerTypeDescriptorHelper() } } - public static IHandlerTypeDescriptor GetHandlerTypeDescriptor(string method) => KnownHandlers.TryGetValue(method, out var descriptor) ? descriptor : null; - - public static IHandlerTypeDescriptor GetHandlerTypeDescriptor() => - KnownHandlers.Values.FirstOrDefault(x => x.InterfaceType == typeof(T)) ?? - GetHandlerTypeDescriptor(GetMethodName(typeof(T))); + public static IHandlerTypeDescriptor GetHandlerTypeDescriptor() => GetHandlerTypeDescriptor(typeof(T)); public static IHandlerTypeDescriptor GetHandlerTypeDescriptor(Type type) { - var @default = KnownHandlers.Values.FirstOrDefault(x => x.InterfaceType == type); + var @default = KnownHandlers + .SelectMany(g => g) + .FirstOrDefault(x => x.InterfaceType == type || x.HandlerType == type || x.ParamsType == type); if (@default != null) { return @default; } var methodName = GetMethodName(type); - if (string.IsNullOrWhiteSpace(methodName)) return null; - return GetHandlerTypeDescriptor(methodName); + return string.IsNullOrWhiteSpace(methodName) ? null : KnownHandlers[methodName].FirstOrDefault(); } - public static string GetMethodName() - where T : IJsonRpcHandler => - GetMethodName(typeof(T)); + public static string GetMethodName() where T : IJsonRpcHandler => GetMethodName(typeof(T)); public static bool IsMethodName(string name, params Type[] types) => types.Any(z => GetMethodName(z).Equals(name)); @@ -78,16 +74,13 @@ public static string GetMethodName(Type type) // Custom method var attribute = MethodAttribute.From(type); - var handler = KnownHandlers.Values.FirstOrDefault( - z => - z.InterfaceType == type || z.HandlerType == type || z.ParamsType == type - ); + var handler = KnownHandlers.SelectMany(z => z) + .FirstOrDefault(z => z.InterfaceType == type || z.HandlerType == type || z.ParamsType == type); if (handler != null) { return handler.Method; } - // TODO: Log unknown method name if (attribute is null) { diff --git a/src/JsonRpc/InputHandler.cs b/src/JsonRpc/InputHandler.cs index f9a3f7c16..aa1b2c577 100644 --- a/src/JsonRpc/InputHandler.cs +++ b/src/JsonRpc/InputHandler.cs @@ -475,86 +475,87 @@ private SchedulerDelegate RouteRequest(IRequestDescriptor de var cts = new CancellationTokenSource(); _requests.TryAdd(request.Id, ( cts, descriptors )); - return (contentModifiedToken, scheduler) => Observable.Create( - observer => { - // ITS A RACE! - var sub = Observable.Amb( - contentModifiedToken.Select( - _ => { - _logger.LogTrace( - "Request {Id} was abandoned due to content be modified", request.Id - ); - return new ErrorResponse( - new ContentModified(request.Id, request.Method) - ); - } - ), - Observable.Timer(_requestTimeout, scheduler).Select( - z => new ErrorResponse(new RequestCancelled(request.Id, request.Method)) - ), - Observable.FromAsync( - async ct => { - using var timer = _logger.TimeDebug( - "Processing request {Method} {ResponseId}", request.Method, - request.Id - ); - ct.Register(cts.Cancel); - // ObservableToToken(contentModifiedToken).Register(cts.Cancel); - try - { - return await _requestRouter.RouteRequest( - descriptors, request, cts.Token - ); - } - catch (OperationCanceledException) - { - _logger.LogTrace("Request {Id} was cancelled", request.Id); - return new RequestCancelled(request.Id, request.Method); - } - catch (RpcErrorException e) - { - _logger.LogCritical( - Events.UnhandledRequest, e, - "Failed to handle request {Method} {RequestId}", request.Method, - request.Id - ); - return new RpcError( - request.Id, request.Method, - new ErrorMessage(e.Code, e.Message, e.Error) - ); - } - catch (Exception e) - { - _logger.LogCritical( - Events.UnhandledRequest, e, - "Failed to handle request {Method} {RequestId}", request.Method, - request.Id - ); - return new InternalError(request.Id, request.Method, e.ToString()); - } - } - ) - ) - .Subscribe(observer); - return new CompositeDisposable { - sub, - Disposable.Create( - () => { - if (_requests.TryRemove(request.Id, out var v)) - { - v.cancellationTokenSource.Dispose(); - } - } - ) - }; - } - ) - .Select( - response => { - _outputHandler.Send(response.Value); - return Unit.Default; - } - ); + return (contentModifiedToken, scheduler) => + Observable.Create( + observer => { + // ITS A RACE! + var sub = Observable.Amb( + contentModifiedToken.Select( + _ => { + _logger.LogTrace( + "Request {Id} was abandoned due to content be modified", request.Id + ); + return new ErrorResponse( + new ContentModified(request.Id, request.Method) + ); + } + ), + Observable.Timer(_requestTimeout, scheduler).Select( + z => new ErrorResponse(new RequestCancelled(request.Id, request.Method)) + ), + Observable.FromAsync( + async ct => { + using var timer = _logger.TimeDebug( + "Processing request {Method} {ResponseId}", request.Method, + request.Id + ); + ct.Register(cts.Cancel); + // ObservableToToken(contentModifiedToken).Register(cts.Cancel); + try + { + return await _requestRouter.RouteRequest( + descriptors, request, cts.Token + ); + } + catch (OperationCanceledException) + { + _logger.LogTrace("Request {Id} was cancelled", request.Id); + return new RequestCancelled(request.Id, request.Method); + } + catch (RpcErrorException e) + { + _logger.LogCritical( + Events.UnhandledRequest, e, + "Failed to handle request {Method} {RequestId}", request.Method, + request.Id + ); + return new RpcError( + request.Id, request.Method, + new ErrorMessage(e.Code, e.Message, e.Error) + ); + } + catch (Exception e) + { + _logger.LogCritical( + Events.UnhandledRequest, e, + "Failed to handle request {Method} {RequestId}", request.Method, + request.Id + ); + return new InternalError(request.Id, request.Method, e.ToString()); + } + } + ) + ) + .Subscribe(observer); + return new CompositeDisposable { + sub, + Disposable.Create( + () => { + if (_requests.TryRemove(request.Id, out var v)) + { + v.cancellationTokenSource.Dispose(); + } + } + ) + }; + } + ) + .Select( + response => { + _outputHandler.Send(response.Value); + return Unit.Default; + } + ); } private SchedulerDelegate RouteNotification(IRequestDescriptor descriptors, Notification notification) => diff --git a/src/Protocol/Models/ExecuteCommandParams.cs b/src/Protocol/Models/ExecuteCommandParams.cs index 27e9abfd8..1e17022ed 100644 --- a/src/Protocol/Models/ExecuteCommandParams.cs +++ b/src/Protocol/Models/ExecuteCommandParams.cs @@ -6,7 +6,7 @@ namespace OmniSharp.Extensions.LanguageServer.Protocol.Models { [Method(WorkspaceNames.ExecuteCommand, Direction.ClientToServer)] - public class ExecuteCommandParams : IRequest, IWorkDoneProgressParams + public class ExecuteCommandParams : IRequest, IWorkDoneProgressParams, IExecuteCommandParams { /// /// The identifier of the actual command handler. diff --git a/src/Protocol/Models/IExecuteCommandParams.cs b/src/Protocol/Models/IExecuteCommandParams.cs new file mode 100644 index 000000000..1b5b63314 --- /dev/null +++ b/src/Protocol/Models/IExecuteCommandParams.cs @@ -0,0 +1,18 @@ +using Newtonsoft.Json.Linq; +using OmniSharp.Extensions.JsonRpc; + +namespace OmniSharp.Extensions.LanguageServer.Protocol.Models +{ + public interface IExecuteCommandParams + { + /// + /// The identifier of the actual command handler. + /// + string Command { get; set; } + + /// + /// Arguments that the command should be invoked with. + /// + JArray Arguments { get; set; } + } +} diff --git a/src/Protocol/Serialization/OptionalAttribute.cs b/src/Protocol/Serialization/OptionalAttribute.cs index e00916d22..0497156cc 100644 --- a/src/Protocol/Serialization/OptionalAttribute.cs +++ b/src/Protocol/Serialization/OptionalAttribute.cs @@ -3,7 +3,7 @@ namespace OmniSharp.Extensions.LanguageServer.Protocol.Serialization { [AttributeUsage(AttributeTargets.Property)] - internal class OptionalAttribute : Attribute + public class OptionalAttribute : Attribute { } } diff --git a/src/Protocol/Shared/LspHandlerTypeDescriptorHelper.cs b/src/Protocol/Shared/LspHandlerTypeDescriptorHelper.cs index 8c57baf4d..fd25d4852 100644 --- a/src/Protocol/Shared/LspHandlerTypeDescriptorHelper.cs +++ b/src/Protocol/Shared/LspHandlerTypeDescriptorHelper.cs @@ -12,15 +12,15 @@ public static class LspHandlerTypeDescriptorHelper private static readonly ConcurrentDictionary MethodNames = new ConcurrentDictionary(); - private static readonly ImmutableSortedDictionary KnownHandlers; + private static readonly ILookup KnownHandlers; static LspHandlerTypeDescriptorHelper() { try { - KnownHandlers = HandlerTypeDescriptorHelper.KnownHandlers.Values + KnownHandlers = HandlerTypeDescriptorHelper.KnownHandlers.SelectMany(x => x) .Select(x => new LspHandlerTypeDescriptor(x.HandlerType) as ILspHandlerTypeDescriptor) - .ToImmutableSortedDictionary(x => x.Method, x => x, StringComparer.Ordinal); + .ToLookup(x => x.Method, x => x, StringComparer.Ordinal); } catch (Exception e) { @@ -28,7 +28,7 @@ static LspHandlerTypeDescriptorHelper() } } - public static ILspHandlerTypeDescriptor GetHandlerTypeForRegistrationOptions(object registrationOptions) + public static string GetMethodForRegistrationOptions(object registrationOptions) { var registrationType = registrationOptions.GetType(); var interfaces = new HashSet( @@ -37,30 +37,34 @@ public static ILspHandlerTypeDescriptor GetHandlerTypeForRegistrationOptions(obj ); return interfaces.SelectMany( x => - KnownHandlers.Values + KnownHandlers.SelectMany(z => z) .Where(z => z.HasRegistration) .Where(z => x.IsAssignableFrom(z.RegistrationType)) ) - .FirstOrDefault(); + .FirstOrDefault()?.Method; } - public static ILspHandlerTypeDescriptor GetHandlerTypeDescriptor(string method) => KnownHandlers.TryGetValue(method, out var descriptor) ? descriptor : null; - - public static ILspHandlerTypeDescriptor GetHandlerTypeDescriptor() => - KnownHandlers.Values.FirstOrDefault(x => x.InterfaceType == typeof(T)) ?? - GetHandlerTypeDescriptor(HandlerTypeDescriptorHelper.GetMethodName(typeof(T))); + public static Type GetRegistrationType(string method) => KnownHandlers[method] + .Where(z => z.HasRegistration) + .Select(z => z.RegistrationType) + .FirstOrDefault(); + public static ILspHandlerTypeDescriptor GetHandlerTypeDescriptor() => GetHandlerTypeDescriptor(typeof(T)); public static ILspHandlerTypeDescriptor GetHandlerTypeDescriptor(Type type) { - var @default = KnownHandlers.Values.FirstOrDefault(x => x.InterfaceType == type); + var @default = KnownHandlers + .SelectMany(g => g) + .FirstOrDefault(x => x.InterfaceType == type || x.HandlerType == type || x.ParamsType == type) + ?? KnownHandlers + .SelectMany(g => g) + .FirstOrDefault(x => x.InterfaceType.IsAssignableFrom(type) || x.HandlerType.IsAssignableFrom(type)); if (@default != null) { return @default; } var methodName = HandlerTypeDescriptorHelper.GetMethodName(type); - if (string.IsNullOrWhiteSpace(methodName)) return null; - return GetHandlerTypeDescriptor(methodName); + return string.IsNullOrWhiteSpace(methodName) ? null : KnownHandlers[methodName].FirstOrDefault(); } } } diff --git a/src/Server/Matchers/ExecuteCommandMatcher.cs b/src/Server/Matchers/ExecuteCommandMatcher.cs index 6a94bcb9c..62f0b7396 100644 --- a/src/Server/Matchers/ExecuteCommandMatcher.cs +++ b/src/Server/Matchers/ExecuteCommandMatcher.cs @@ -21,7 +21,7 @@ public class ExecuteCommandMatcher : IHandlerMatcher /// public IEnumerable FindHandler(object parameters, IEnumerable descriptors) { - if (parameters is ExecuteCommandParams executeCommandParams) + if (parameters is IExecuteCommandParams executeCommandParams) { _logger.LogTrace("Registration options {OptionsName}", executeCommandParams.GetType().FullName); foreach (var descriptor in descriptors) diff --git a/src/Shared/LspRequestRouter.cs b/src/Shared/LspRequestRouter.cs index 28347f113..12f7b5ac2 100644 --- a/src/Shared/LspRequestRouter.cs +++ b/src/Shared/LspRequestRouter.cs @@ -71,7 +71,7 @@ private IRequestDescriptor FindDescriptor(string method, // execute command is a special case // if no command was found to execute this must error // this is not great coupling but other options require api changes - if (paramsValue is ExecuteCommandParams) return new RequestDescriptor(); + if (paramsValue is IExecuteCommandParams) return new RequestDescriptor(); if (lspHandlerDescriptors.Count > 0) return new RequestDescriptor(lspHandlerDescriptors); return new RequestDescriptor(); } diff --git a/src/Shared/SharedHandlerCollection.cs b/src/Shared/SharedHandlerCollection.cs index 0f3adc81b..1002c2a61 100644 --- a/src/Shared/SharedHandlerCollection.cs +++ b/src/Shared/SharedHandlerCollection.cs @@ -215,7 +215,7 @@ private LspHandlerDescriptor GetDescriptor(string method, Type handlerType, ISer private LspHandlerDescriptor GetDescriptor(string method, Type handlerType, IJsonRpcHandler handler, JsonRpcHandlerOptions options) { - var typeDescriptor = LspHandlerTypeDescriptorHelper.GetHandlerTypeDescriptor(method); + var typeDescriptor = LspHandlerTypeDescriptorHelper.GetHandlerTypeDescriptor(handlerType); var @interface = HandlerTypeDescriptorHelper.GetHandlerInterface(handlerType); var registrationType = typeDescriptor?.RegistrationType ?? HandlerTypeDescriptorHelper.UnwrapGenericType(typeof(IRegistration<>), handlerType); diff --git a/test/Lsp.Tests/Integration/OverrideHandlerTests.cs b/test/Lsp.Tests/Integration/OverrideHandlerTests.cs new file mode 100644 index 000000000..b88b96410 --- /dev/null +++ b/test/Lsp.Tests/Integration/OverrideHandlerTests.cs @@ -0,0 +1,112 @@ +using System.Threading; +using System.Threading.Tasks; +using FluentAssertions; +using MediatR; +using Newtonsoft.Json.Linq; +using NSubstitute; +using OmniSharp.Extensions.JsonRpc; +using OmniSharp.Extensions.JsonRpc.Testing; +using OmniSharp.Extensions.LanguageProtocol.Testing; +using OmniSharp.Extensions.LanguageServer.Protocol; +using OmniSharp.Extensions.LanguageServer.Protocol.Client.Capabilities; +using OmniSharp.Extensions.LanguageServer.Protocol.Models; +using OmniSharp.Extensions.LanguageServer.Protocol.Serialization; +using OmniSharp.Extensions.LanguageServer.Protocol.Workspace; +using Xunit; +using Xunit.Abstractions; + +namespace Lsp.Tests.Integration +{ + public class OverrideHandlerTests : LanguageProtocolTestBase + { + public OverrideHandlerTests(ITestOutputHelper testOutputHelper) : base(new JsonRpcTestOptions().ConfigureForXUnit(testOutputHelper)) + { + } + + [Fact] + public async Task Should_Support_Custom_Execute_Command_Handlers() + { + var (client, server) = await Initialize( + options => { + + }, options => { + options.AddHandler(); + } + ); + + var response = await client.SendRequest( + new CustomExecuteCommandParams() { + Command = "mycommand", + }, CancellationToken + ); + + response.Should().BeEquivalentTo(JToken.FromObject(new { someValue = "custom" })); + } + + [Fact] + public async Task Should_Support_Mixed_Execute_Command_Handlers() + { + var (client, server) = await Initialize( + options => { + + }, options => { + options.AddHandler(); + options.OnExecuteCommand("myothercommand", (a, ct) => Unit.Task); + } + ); + + var normalResponse = await client.SendRequest( + new ExecuteCommandParams() { + Command = "myothercommand", + Arguments = new JArray(new JObject()) + }, CancellationToken + ); + + var customResponse = await client.SendRequest( + new CustomExecuteCommandParams() { + Command = "mycommand", + }, CancellationToken + ); + + normalResponse.Should().Be(Unit.Value); + customResponse.Should().BeEquivalentTo(JToken.FromObject(new { someValue = "custom" })); + } + + [Method(WorkspaceNames.ExecuteCommand)] + public class CustomExecuteCommandHandler : IJsonRpcRequestHandler, IRegistration, ICapability + { + private ExecuteCommandCapability _capability; + private readonly ExecuteCommandRegistrationOptions _executeCommandRegistrationOptions = new ExecuteCommandRegistrationOptions() { + WorkDoneProgress = true, + Commands = new Container("mycommand") + }; + + public Task Handle(CustomExecuteCommandParams request, CancellationToken cancellationToken) + { + return Task.FromResult(JToken.FromObject(new { someValue = "custom" })); + } + + public ExecuteCommandRegistrationOptions GetRegistrationOptions() { return _executeCommandRegistrationOptions; } + public void SetCapability(ExecuteCommandCapability capability) => _capability = capability; + } + + [Method(WorkspaceNames.ExecuteCommand, Direction.ClientToServer)] + public class CustomExecuteCommandParams : IRequest, IWorkDoneProgressParams, IExecuteCommandParams // required for routing + { + /// + /// The identifier of the actual command handler. + /// + public string Command { get; set; } + + /// + /// Arguments that the command should be invoked with. + /// + [Optional] + public JArray Arguments { get; set; } + + /// + [Optional] + public ProgressToken WorkDoneToken { get; set; } + } + } +} diff --git a/test/Lsp.Tests/Lsp.Tests.csproj b/test/Lsp.Tests/Lsp.Tests.csproj index 0f388f8fb..232379821 100644 --- a/test/Lsp.Tests/Lsp.Tests.csproj +++ b/test/Lsp.Tests/Lsp.Tests.csproj @@ -7,6 +7,8 @@ + + @@ -15,7 +17,4 @@ - - - From 51b863d594585f522a95c59970459f80d012405c Mon Sep 17 00:00:00 2001 From: David Driscoll Date: Wed, 12 Aug 2020 22:22:44 -0400 Subject: [PATCH 2/2] fix project file --- test/Lsp.Tests/Lsp.Tests.csproj | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/Lsp.Tests/Lsp.Tests.csproj b/test/Lsp.Tests/Lsp.Tests.csproj index 232379821..52ee55e47 100644 --- a/test/Lsp.Tests/Lsp.Tests.csproj +++ b/test/Lsp.Tests/Lsp.Tests.csproj @@ -7,8 +7,6 @@ - -