From 03d7b659729436f0557a1e562f032e9702d1cab2 Mon Sep 17 00:00:00 2001 From: David Driscoll Date: Wed, 21 Oct 2020 10:18:12 -0400 Subject: [PATCH] Custom handlers not override built in handlers correctly (#392) * Fixed a bug in configuration (configuration values were case-sensitive). Added ability to disable default handlers for language client / language server. Added unit tests demonstrating Configuration.Binder, and Options usages * fixed an issue where the MedatR handler was being registered in the container for 'built-in' handlers, these were overriding any custom ones that were added later * added default request handler, and decorator to ensure that if a IRequestContext is given, then that handler is used. * removed some changes that are no longer needed * Removed extra dependencies, we can add these at another time * cleanup * removed unneeded code * Added additional assertions --- Directory.Build.targets | 5 +- src/Client/LanguageClientOptions.cs | 4 +- ...nguageClientServiceCollectionExtensions.cs | 5 +- .../GenerateHandlerMethodsGenerator.cs | 5 +- ...sonRpcServerServiceCollectionExtensions.cs | 60 ++++-- .../ConfigureByConfigurationPathExtension.cs | 176 ++++++++++++++++++ .../LanguageProtocolDelegatingHandlers.cs | 11 ++ src/Protocol/Protocol.csproj | 5 + .../IDidChangeConfigurationHandler.cs | 5 +- .../IDidChangeWorkspaceFoldersHandler.cs | 3 +- .../DidChangeConfigurationProvider.cs | 2 +- src/Server/DefaultLanguageServerFacade.cs | 2 +- src/Server/LanguageServer.cs | 2 +- src/Server/LanguageServerHelpers.cs | 34 +++- src/Server/LanguageServerOptions.cs | 4 +- ...nguageServerServiceCollectionExtensions.cs | 8 +- test/Lsp.Tests/FluentAssertionsExtensions.cs | 52 ++++++ .../Integration/DisableDefaultsTests.cs | 159 ++++++++++++++++ .../LanguageServerConfigurationTests.cs | 91 ++++++++- test/Lsp.Tests/Lsp.Tests.csproj | 9 + 20 files changed, 600 insertions(+), 42 deletions(-) create mode 100644 src/Protocol/ConfigureByConfigurationPathExtension.cs create mode 100644 test/Lsp.Tests/Integration/DisableDefaultsTests.cs diff --git a/Directory.Build.targets b/Directory.Build.targets index 4575754d0..a120fa374 100644 --- a/Directory.Build.targets +++ b/Directory.Build.targets @@ -17,6 +17,9 @@ + + + @@ -45,4 +48,4 @@ - \ No newline at end of file + diff --git a/src/Client/LanguageClientOptions.cs b/src/Client/LanguageClientOptions.cs index ba77406c8..69483f8f2 100644 --- a/src/Client/LanguageClientOptions.cs +++ b/src/Client/LanguageClientOptions.cs @@ -18,6 +18,7 @@ public LanguageClientOptions() { WithAssemblies(typeof(LanguageClientOptions).Assembly, typeof(LspRequestRouter).Assembly); } + public ClientCapabilities ClientCapabilities { get; set; } = new ClientCapabilities { Experimental = new Dictionary(), Window = new WindowClientCapabilities(), @@ -52,7 +53,8 @@ ILanguageClientRegistry IJsonRpcHandlerRegistry.AddHand ILanguageClientRegistry IJsonRpcHandlerRegistry.AddHandler(JsonRpcHandlerFactory handlerFunc, JsonRpcHandlerOptions? options) => AddHandler(handlerFunc, options); - ILanguageClientRegistry IJsonRpcHandlerRegistry.AddHandler(IJsonRpcHandler handler, JsonRpcHandlerOptions? options) => AddHandler(handler, options); + ILanguageClientRegistry IJsonRpcHandlerRegistry.AddHandler(IJsonRpcHandler handler, JsonRpcHandlerOptions? options) => + AddHandler(handler, options); ILanguageClientRegistry IJsonRpcHandlerRegistry.AddHandler(JsonRpcHandlerOptions? options) => AddHandler(options); diff --git a/src/Client/LanguageClientServiceCollectionExtensions.cs b/src/Client/LanguageClientServiceCollectionExtensions.cs index 7fec5fc1d..3f4564135 100644 --- a/src/Client/LanguageClientServiceCollectionExtensions.cs +++ b/src/Client/LanguageClientServiceCollectionExtensions.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Reflection; using DryIoc; +using MediatR; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; @@ -28,7 +29,7 @@ internal static IContainer AddLanguageClientInternals(this IContainer container, nonPublicServiceTypes: true, ifAlreadyRegistered: IfAlreadyRegistered.Keep ); - if (!EqualityComparer.Default.Equals( options.OnUnhandledException, default)) + if (!EqualityComparer.Default.Equals(options.OnUnhandledException, default)) { container.RegisterInstance(options.OnUnhandledException); } @@ -79,7 +80,7 @@ internal static IContainer AddLanguageClientInternals(this IContainer container, if (providedConfiguration != null) { - builder.CustomAddConfiguration((providedConfiguration.ImplementationInstance as IConfiguration)!); + builder.CustomAddConfiguration(( providedConfiguration.ImplementationInstance as IConfiguration )!); } //var didChangeConfigurationProvider = _.GetRequiredService(); diff --git a/src/JsonRpc.Generators/GenerateHandlerMethodsGenerator.cs b/src/JsonRpc.Generators/GenerateHandlerMethodsGenerator.cs index b5075b059..a207514ae 100644 --- a/src/JsonRpc.Generators/GenerateHandlerMethodsGenerator.cs +++ b/src/JsonRpc.Generators/GenerateHandlerMethodsGenerator.cs @@ -171,8 +171,9 @@ MemberDeclarationSyntax MakeAction(TypeSyntax syntax) yield return MakeAction(CreateAsyncAction(true, requestType)); if (capability != null) { - yield return MakeAction(CreateAction(requestType, capability)); - yield return MakeAction(CreateAsyncAction(requestType, capability)); + method = method.WithExpressionBody( + GetNotificationCapabilityHandlerExpression(GetMethodName(handlerInterface), requestType, capability) + ); yield return MakeAction(CreateAction(requestType, capability)); yield return MakeAction(CreateAsyncAction(requestType, capability)); } diff --git a/src/JsonRpc/JsonRpcServerServiceCollectionExtensions.cs b/src/JsonRpc/JsonRpcServerServiceCollectionExtensions.cs index 234193771..b27a6e475 100644 --- a/src/JsonRpc/JsonRpcServerServiceCollectionExtensions.cs +++ b/src/JsonRpc/JsonRpcServerServiceCollectionExtensions.cs @@ -1,6 +1,8 @@ using System; using System.IO.Pipelines; using System.Linq; +using System.Threading; +using System.Threading.Tasks; using DryIoc; using MediatR; using Microsoft.Extensions.DependencyInjection; @@ -79,23 +81,51 @@ internal static IContainer AddJsonRpcMediatR(this IContainer container) container.RegisterMany(new[] { typeof(IMediator).GetAssembly() }, Registrator.Interfaces, Reuse.ScopedOrSingleton); container.RegisterMany(Reuse.Scoped); container.RegisterDelegate(context => context.Resolve, Reuse.ScopedOrSingleton); + container.Register(typeof(IRequestHandler<,>), typeof(RequestHandler<,>)); + container.Register(typeof(IRequestHandler<,>), typeof(RequestHandlerDecorator<,>), setup: Setup.Decorator); + + return container; + } + + class RequestHandler : IRequestHandler where T : IRequest + { + private readonly IRequestContext _requestContext; + + public RequestHandler(IRequestContext requestContext) + { + _requestContext = requestContext; + } + public Task Handle(T request, CancellationToken cancellationToken) + { + return ((IRequestHandler) _requestContext.Descriptor.Handler).Handle(request, cancellationToken); + } + } + + class RequestHandlerDecorator : IRequestHandler where T : IRequest + { + private readonly IRequestHandler? _handler; + private readonly IRequestContext? _requestContext; + + public RequestHandlerDecorator(IRequestHandler? handler = null, IRequestContext? requestContext = null) + { + _handler = handler; + _requestContext = requestContext; + } + public Task Handle(T request, CancellationToken cancellationToken) + { + if (_requestContext == null) + { + if (_handler == null) + { + throw new NotImplementedException($"No request handler was registered for type {typeof(IRequestHandler).FullName}"); - return container.With( - rules => rules.WithUnknownServiceResolvers( - request => { - if (request.ServiceType.IsGenericType && typeof(IRequestHandler<,>).IsAssignableFrom(request.ServiceType.GetGenericTypeDefinition())) - { - var context = request.Container.Resolve(); - if (context != null) - { - return new RegisteredInstanceFactory(context.Descriptor.Handler); - } - } - - return null; } - ) - ); + + return _handler.Handle(request, cancellationToken); + } + + return ((IRequestHandler) _requestContext.Descriptor.Handler).Handle(request, cancellationToken); + } } internal static IContainer AddJsonRpcServerInternals(this IContainer container, JsonRpcServerOptions options) diff --git a/src/Protocol/ConfigureByConfigurationPathExtension.cs b/src/Protocol/ConfigureByConfigurationPathExtension.cs new file mode 100644 index 000000000..336c9e6dd --- /dev/null +++ b/src/Protocol/ConfigureByConfigurationPathExtension.cs @@ -0,0 +1,176 @@ +#if false +using System; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Configuration.Binder; +using Microsoft.Extensions.Primitives; + +// ReSharper disable once CheckNamespace +namespace Microsoft.Extensions.Configuration +{ + public static class ConfigureByConfigurationPathExtension + { + /// + /// Registers a injected configuration service which TOptions will bind against. + /// + /// The type of options being configured. + /// The to add the services to. + /// The so that additional calls can be chained. + public static IServiceCollection Configure(this IServiceCollection services) + where TOptions : class + { + if (services == null) + { + throw new ArgumentNullException(nameof(services)); + } + + return Configure(services, null); + } + + /// + /// Registers a injected configuration service which TOptions will bind against. + /// + /// The type of options being configured. + /// The to add the services to. + /// The name of the options instance. + /// The so that additional calls can be chained. + public static IServiceCollection Configure(this IServiceCollection services, string? sectionName) + where TOptions : class + { + if (services == null) + { + throw new ArgumentNullException(nameof(services)); + } + + services.AddOptions(); + services.AddSingleton>( + _ => new ConfigurationChangeTokenSource( + Options.Options.DefaultName, + sectionName == null ? _.GetRequiredService() : _.GetRequiredService().GetSection(sectionName) + ) + ); + return services.AddSingleton>( + _ => new NamedConfigureFromConfigurationOptions( + Options.Options.DefaultName, + sectionName == null ? _.GetRequiredService() : _.GetRequiredService().GetSection(sectionName) + ) + ); + } + + /// + /// Registers a injected configuration service which TOptions will bind against. + /// + /// The type of options being configured. + /// The to add the services to. + /// Used to configure the . + /// The so that additional calls can be chained. + public static IServiceCollection Configure(this IServiceCollection services, Action configureBinder) + where TOptions : class + { + if (services == null) + { + throw new ArgumentNullException(nameof(services)); + } + + return Configure(services, Options.Options.DefaultName, configureBinder); + } + + /// + /// Registers a injected configuration service which TOptions will bind against. + /// + /// The type of options being configured. + /// The to add the services to. + /// The name of the options instance. + /// Used to configure the . + /// The so that additional calls can be chained. + public static IServiceCollection Configure(this IServiceCollection services, string sectionName, Action configureBinder) + where TOptions : class + { + if (services == null) + { + throw new ArgumentNullException(nameof(services)); + } + + services.AddOptions(); + services.AddSingleton>(_ => new ConfigurationChangeTokenSource(Options.Options.DefaultName, _.GetRequiredService().GetSection(sectionName))); + return services.AddSingleton>(_ => new NamedConfigureFromConfigurationOptions(Options.Options.DefaultName, _.GetRequiredService().GetSection(sectionName), configureBinder)); + } + + /// + /// Registers a injected configuration service which TOptions will bind against. + /// + /// The type of options being configured. + /// The to configure. + /// The so that additional calls can be chained. + public static OptionsBuilder Configure(this OptionsBuilder builder) + where TOptions : class + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + return Configure(builder, Options.Options.DefaultName); + } + + /// + /// Registers a injected configuration service which TOptions will bind against. + /// + /// The type of options being configured. + /// The to configure. + /// The name of the options instance. + /// The so that additional calls can be chained. + public static OptionsBuilder Configure(this OptionsBuilder builder, string sectionName) + where TOptions : class + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + Configure(builder.Services, name); + return builder; + } + + /// + /// Registers a injected configuration service which TOptions will bind against. + /// + /// The type of options being configured. + /// The to configure. + /// Used to configure the . + /// The so that additional calls can be chained. + public static OptionsBuilder Configure(this OptionsBuilder builder, Action configureBinder) + where TOptions : class + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + return Configure(builder, Options.Options.DefaultName, configureBinder); + } + + /// + /// Registers a injected configuration service which TOptions will bind against. + /// + /// The type of options being configured. + /// The to configure. + /// The name of the options instance. + /// Used to configure the . + /// The so that additional calls can be chained. + public static OptionsBuilder Configure(this OptionsBuilder builder, string sectionName, Action configureBinder) + where TOptions : class + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + + Configure(builder.Services, name, configureBinder); + return builder; + } + } +} +#endif diff --git a/src/Protocol/LanguageProtocolDelegatingHandlers.cs b/src/Protocol/LanguageProtocolDelegatingHandlers.cs index 2136ca6ee..eba50a456 100644 --- a/src/Protocol/LanguageProtocolDelegatingHandlers.cs +++ b/src/Protocol/LanguageProtocolDelegatingHandlers.cs @@ -1088,6 +1088,17 @@ public NotificationCapability(Func handler) : { } + public NotificationCapability(Action handler) : + this( + Guid.Empty, (request, capability, ct) => { + handler(request, capability, ct); + return Task.CompletedTask; + } + ) + { + } + + public NotificationCapability(Func handler) : this(Guid.Empty, handler) { diff --git a/src/Protocol/Protocol.csproj b/src/Protocol/Protocol.csproj index 00c78fc5e..0114e9d23 100644 --- a/src/Protocol/Protocol.csproj +++ b/src/Protocol/Protocol.csproj @@ -9,6 +9,11 @@ + diff --git a/src/Protocol/Workspace/IDidChangeConfigurationHandler.cs b/src/Protocol/Workspace/IDidChangeConfigurationHandler.cs index 7f80e3fa9..789d699e8 100644 --- a/src/Protocol/Workspace/IDidChangeConfigurationHandler.cs +++ b/src/Protocol/Workspace/IDidChangeConfigurationHandler.cs @@ -14,15 +14,16 @@ namespace OmniSharp.Extensions.LanguageServer.Protocol.Workspace [GenerateHandlerMethods] [GenerateRequestMethods(typeof(IWorkspaceLanguageClient), typeof(ILanguageClient))] public interface IDidChangeConfigurationHandler : IJsonRpcNotificationHandler, - IRegistration, ICapability + IRegistration, // TODO: Remove this in the future + ICapability { } public abstract class DidChangeConfigurationHandler : IDidChangeConfigurationHandler { - public object GetRegistrationOptions() => new object(); public abstract Task Handle(DidChangeConfigurationParams request, CancellationToken cancellationToken); public virtual void SetCapability(DidChangeConfigurationCapability capability) => Capability = capability; protected DidChangeConfigurationCapability Capability { get; private set; } = null!; + public object GetRegistrationOptions() => new object(); } } diff --git a/src/Protocol/Workspace/IDidChangeWorkspaceFoldersHandler.cs b/src/Protocol/Workspace/IDidChangeWorkspaceFoldersHandler.cs index f8ae20910..60b2d7812 100644 --- a/src/Protocol/Workspace/IDidChangeWorkspaceFoldersHandler.cs +++ b/src/Protocol/Workspace/IDidChangeWorkspaceFoldersHandler.cs @@ -12,7 +12,8 @@ namespace OmniSharp.Extensions.LanguageServer.Protocol.Workspace [Method(WorkspaceNames.DidChangeWorkspaceFolders, Direction.ClientToServer)] [GenerateHandlerMethods] [GenerateRequestMethods(typeof(IWorkspaceLanguageClient), typeof(ILanguageClient))] - public interface IDidChangeWorkspaceFoldersHandler : IJsonRpcNotificationHandler, IRegistration + public interface IDidChangeWorkspaceFoldersHandler : IJsonRpcNotificationHandler, + IRegistration // TODO: Remove this in the future { } diff --git a/src/Server/Configuration/DidChangeConfigurationProvider.cs b/src/Server/Configuration/DidChangeConfigurationProvider.cs index aeedb2dfd..6688971a8 100644 --- a/src/Server/Configuration/DidChangeConfigurationProvider.cs +++ b/src/Server/Configuration/DidChangeConfigurationProvider.cs @@ -96,7 +96,7 @@ public Task Handle(DidChangeConfigurationParams request, CancellationToken return Concat( Create( observer => { - var newData = new Dictionary(); + var newData = new Dictionary(StringComparer.OrdinalIgnoreCase); // configuration is case-insensitive return GetConfigurationFromClient(_configurationItems) .Select( x => { diff --git a/src/Server/DefaultLanguageServerFacade.cs b/src/Server/DefaultLanguageServerFacade.cs index e2ba1ad5d..3331596f3 100644 --- a/src/Server/DefaultLanguageServerFacade.cs +++ b/src/Server/DefaultLanguageServerFacade.cs @@ -71,7 +71,7 @@ public IDisposable Register(Action registryAction) var result = manager.GetDisposable(); if (_instancesHasStarted.Started) { - LanguageServerHelpers.InitHandlers(ResolverContext.Resolve(), result); + LanguageServerHelpers.InitHandlers(ResolverContext.Resolve(), result, _supportedCapabilities.Value); } return LanguageServerHelpers.RegisterHandlers(_hasStarted, Client, _workDoneManager.Value, _supportedCapabilities.Value, result); diff --git a/src/Server/LanguageServer.cs b/src/Server/LanguageServer.cs index e7cee98db..5e95f44df 100644 --- a/src/Server/LanguageServer.cs +++ b/src/Server/LanguageServer.cs @@ -500,7 +500,7 @@ public IDisposable Register(Action registryAction) var result = manager.GetDisposable(); if (_instanceHasStarted.Started) { - LanguageServerHelpers.InitHandlers(this, result); + LanguageServerHelpers.InitHandlers(this, result, _supportedCapabilities); } return LanguageServerHelpers.RegisterHandlers(_initializeComplete.Select(z => System.Reactive.Unit.Default), Client, WorkDoneManager, _supportedCapabilities, result); diff --git a/src/Server/LanguageServerHelpers.cs b/src/Server/LanguageServerHelpers.cs index 5c480c096..662cb5889 100644 --- a/src/Server/LanguageServerHelpers.cs +++ b/src/Server/LanguageServerHelpers.cs @@ -5,6 +5,7 @@ using System.Reactive.Disposables; using System.Reactive.Linq; using System.Reactive.Threading.Tasks; +using OmniSharp.Extensions.JsonRpc; using OmniSharp.Extensions.LanguageServer.Protocol; using OmniSharp.Extensions.LanguageServer.Protocol.Client; using OmniSharp.Extensions.LanguageServer.Protocol.Models; @@ -18,29 +19,42 @@ namespace OmniSharp.Extensions.LanguageServer.Server static class LanguageServerHelpers { static IEnumerable GetUniqueHandlers(CompositeDisposable disposable) + { + return GetUniqueHandlers(disposable).OfType(); + } + + static IEnumerable GetUniqueHandlers(CompositeDisposable disposable) + { + return GetAllDescriptors(disposable) + .Concat(disposable.OfType().SelectMany(GetAllDescriptors)) + .Concat(disposable.OfType().SelectMany(GetAllDescriptors)) + .Select(z => z.Handler) + .Distinct(); + } + + static IEnumerable GetAllDescriptors(CompositeDisposable disposable) { return disposable.OfType() - .Select(z => z.Handler) - .OfType() - .Concat(disposable.OfType().SelectMany(GetUniqueHandlers)) - .Concat(disposable.OfType().SelectMany(GetLspHandlers)) + .Concat(disposable.OfType().SelectMany(GetAllDescriptors)) + .Concat(disposable.OfType().SelectMany(GetAllDescriptors)) .Distinct(); } - static IEnumerable GetLspHandlers(LspHandlerDescriptorDisposable disposable) + static IEnumerable GetAllDescriptors(LspHandlerDescriptorDisposable disposable) { - return disposable.Descriptors - .Select(z => z.Handler) - .OfType() - .Distinct(); + return disposable.Descriptors; } - internal static void InitHandlers(ILanguageServer client, CompositeDisposable result) + internal static void InitHandlers(ILanguageServer client, CompositeDisposable result, ISupportedCapabilities supportedCapabilities ) { Observable.Concat( GetUniqueHandlers(result) .Select(handler => Observable.FromAsync(ct => handler.OnInitialize(client, client.ClientSettings, ct))) .Merge(), + GetAllDescriptors(result) + .Select(item => LspHandlerDescriptorHelpers.InitializeHandler(item, supportedCapabilities, item.Handler)) + .ToObservable() + .Select(z => Unit.Default), GetUniqueHandlers(result) .Select(handler => Observable.FromAsync(ct => handler.OnInitialized(client, client.ClientSettings, client.ServerSettings, ct))) .Merge(), diff --git a/src/Server/LanguageServerOptions.cs b/src/Server/LanguageServerOptions.cs index 16710bf8e..4f332b634 100644 --- a/src/Server/LanguageServerOptions.cs +++ b/src/Server/LanguageServerOptions.cs @@ -16,6 +16,7 @@ public LanguageServerOptions() { WithAssemblies(typeof(LanguageServerOptions).Assembly, typeof(LspRequestRouter).Assembly); } + public ServerInfo? ServerInfo { get; set; } ILanguageServerRegistry IJsonRpcHandlerRegistry.AddHandler(string method, IJsonRpcHandler handler, JsonRpcHandlerOptions? options) => @@ -29,7 +30,8 @@ ILanguageServerRegistry IJsonRpcHandlerRegistry.AddHand ILanguageServerRegistry IJsonRpcHandlerRegistry.AddHandler(JsonRpcHandlerFactory handlerFunc, JsonRpcHandlerOptions? options) => AddHandler(handlerFunc, options); - ILanguageServerRegistry IJsonRpcHandlerRegistry.AddHandler(IJsonRpcHandler handler, JsonRpcHandlerOptions? options) => AddHandler(handler, options); + ILanguageServerRegistry IJsonRpcHandlerRegistry.AddHandler(IJsonRpcHandler handler, JsonRpcHandlerOptions? options) => + AddHandler(handler, options); ILanguageServerRegistry IJsonRpcHandlerRegistry.AddHandler(JsonRpcHandlerOptions? options) => AddHandler(options); diff --git a/src/Server/LanguageServerServiceCollectionExtensions.cs b/src/Server/LanguageServerServiceCollectionExtensions.cs index 03061a356..464f1b89c 100644 --- a/src/Server/LanguageServerServiceCollectionExtensions.cs +++ b/src/Server/LanguageServerServiceCollectionExtensions.cs @@ -2,6 +2,7 @@ using System.Linq; using System.Reflection; using DryIoc; +using MediatR; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; @@ -15,6 +16,7 @@ using OmniSharp.Extensions.LanguageServer.Server.Matchers; using OmniSharp.Extensions.LanguageServer.Server.Pipelines; using OmniSharp.Extensions.LanguageServer.Shared; + #pragma warning disable CS0618 namespace OmniSharp.Extensions.LanguageServer.Server @@ -75,7 +77,7 @@ internal static IContainer AddLanguageServerInternals(this IContainer container, container.RegisterDelegate( _ => { var builder = new ConfigurationBuilder(); - var didChangeConfigurationProvider = _.GetRequiredService(); + var didChangeConfigurationProvider = _.GetRequiredService(); var outerConfiguration = outerServiceProvider?.GetService(); if (outerConfiguration != null) { @@ -84,7 +86,7 @@ internal static IContainer AddLanguageServerInternals(this IContainer container, if (providedConfiguration != null) { - builder.CustomAddConfiguration((providedConfiguration.ImplementationInstance as IConfiguration)!); + builder.CustomAddConfiguration(( providedConfiguration.ImplementationInstance as IConfiguration )!); } return builder.CustomAddConfiguration(didChangeConfigurationProvider).Build(); @@ -108,7 +110,7 @@ internal static IContainer AddLanguageServerInternals(this IContainer container, container.RegisterMany(new[] { typeof(ResolveCommandPipeline<,>) }); container.RegisterMany(new[] { typeof(SemanticTokensDeltaPipeline<,>) }); container.RegisterMany(Reuse.Singleton); - container.RegisterMany(Reuse.Singleton); + container.RegisterMany(reuse: Reuse.Singleton); return container; } diff --git a/test/Lsp.Tests/FluentAssertionsExtensions.cs b/test/Lsp.Tests/FluentAssertionsExtensions.cs index befb2cd27..4c7e47f0d 100644 --- a/test/Lsp.Tests/FluentAssertionsExtensions.cs +++ b/test/Lsp.Tests/FluentAssertionsExtensions.cs @@ -1,6 +1,10 @@ +using System; using FluentAssertions.Equivalency; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; using NSubstitute; using OmniSharp.Extensions.LanguageServer.Protocol; using OmniSharp.Extensions.LanguageServer.Protocol.Client.Capabilities; @@ -9,6 +13,54 @@ namespace Lsp.Tests { public static class FluentAssertionsExtensions { + + /// + /// Registers a injected configuration service which TOptions will bind against. + /// + /// The type of options being configured. + /// The to add the services to. + /// The so that additional calls can be chained. + public static IServiceCollection Configure(this IServiceCollection services) + where TOptions : class + { + if (services == null) + { + throw new ArgumentNullException(nameof(services)); + } + + return Configure(services, null); + } + + /// + /// Registers a injected configuration service which TOptions will bind against. + /// + /// The type of options being configured. + /// The to add the services to. + /// The name of the options instance. + /// The so that additional calls can be chained. + public static IServiceCollection Configure(this IServiceCollection services, string? sectionName) + where TOptions : class + { + if (services == null) + { + throw new ArgumentNullException(nameof(services)); + } + + services.AddOptions(); + services.AddSingleton>( + _ => new ConfigurationChangeTokenSource( + Options.DefaultName, + sectionName == null ? _.GetRequiredService() : _.GetRequiredService().GetSection(sectionName) + ) + ); + return services.AddSingleton>( + _ => new NamedConfigureFromConfigurationOptions( + Options.DefaultName, + sectionName == null ? _.GetRequiredService() : _.GetRequiredService().GetSection(sectionName) + ) + ); + } + public static EquivalencyAssertionOptions ConfigureForSupports(this EquivalencyAssertionOptions options, ILogger? logger = null) => options .WithTracing(new TraceWriter(logger ?? NullLogger.Instance)) diff --git a/test/Lsp.Tests/Integration/DisableDefaultsTests.cs b/test/Lsp.Tests/Integration/DisableDefaultsTests.cs new file mode 100644 index 000000000..d40fdef46 --- /dev/null +++ b/test/Lsp.Tests/Integration/DisableDefaultsTests.cs @@ -0,0 +1,159 @@ +using System; +using System.Collections.Generic; +using System.Composition; +using System.Linq; +using System.Reactive.Linq; +using System.Reactive.Threading.Tasks; +using System.Threading; +using System.Threading.Tasks; +using FluentAssertions; +using Microsoft.Extensions.DependencyInjection; +using Newtonsoft.Json.Linq; +using NSubstitute; +using NSubstitute.Exceptions; +using OmniSharp.Extensions.JsonRpc; +using OmniSharp.Extensions.JsonRpc.Testing; +using OmniSharp.Extensions.LanguageProtocol.Testing; +using OmniSharp.Extensions.LanguageServer.Client; +using OmniSharp.Extensions.LanguageServer.Protocol; +using OmniSharp.Extensions.LanguageServer.Protocol.Client; +using OmniSharp.Extensions.LanguageServer.Protocol.Client.Capabilities; +using OmniSharp.Extensions.LanguageServer.Protocol.Document; +using OmniSharp.Extensions.LanguageServer.Protocol.Models; +using OmniSharp.Extensions.LanguageServer.Protocol.Server; +using OmniSharp.Extensions.LanguageServer.Protocol.Window; +using OmniSharp.Extensions.LanguageServer.Protocol.Workspace; +using OmniSharp.Extensions.LanguageServer.Server; +using OmniSharp.Extensions.LanguageServer.Shared; +using TestingUtils; +using Xunit; +using Xunit.Abstractions; + +namespace Lsp.Tests.Integration +{ + public class DisableDefaultsTests : LanguageProtocolTestBase + { + public DisableDefaultsTests(ITestOutputHelper outputHelper) : base(new JsonRpcTestOptions().ConfigureForXUnit(outputHelper)) + { + } + + [Fact] + public async Task Should_Disable_Registration_Manager() + { + var registrationAction = Substitute.For>(); + var unregistrationAction = Substitute.For>(); + var (client, _) = await Initialize( + options => options + .OnRegisterCapability(registrationAction) + .OnUnregisterCapability(unregistrationAction), + options => { } + ); + + var clientManager = client.Services.GetRequiredService(); + clientManager.Descriptors.Should().Contain(f => f.Handler is DelegatingHandlers.Request); + clientManager.Descriptors.Should().ContainSingle(f => f.Method == ClientNames.RegisterCapability); + clientManager.Descriptors.Should().Contain(f => f.Handler is DelegatingHandlers.Request); + clientManager.Descriptors.Should().ContainSingle(f => f.Method == ClientNames.UnregisterCapability); + } + + [Fact] + public async Task Should_Disable_Workspace_Folder_Manager() + { + var clientAction = Substitute.For?>>>(); + var serverAction = Substitute.For>(); + var (client, server) = await Initialize( + options => options.OnWorkspaceFolders(clientAction), + options => options.OnDidChangeWorkspaceFolders(serverAction, new object()) + ); + + var clientManager = client.Services.GetRequiredService(); + clientManager.Descriptors.Should().Contain(f => f.Handler is DelegatingHandlers.Request?>); + clientManager.Descriptors.Should().ContainSingle(f => f.Method == WorkspaceNames.WorkspaceFolders); + + var serverManager = server.Services.GetRequiredService(); + serverManager.Descriptors.Should().Contain(f => f.Handler is LanguageProtocolDelegatingHandlers.Notification); + serverManager.Descriptors.Should().ContainSingle(f => f.Method == WorkspaceNames.DidChangeWorkspaceFolders); + } + + [Fact] + public async Task Should_Allow_Custom_Workspace_Folder_Manager_Delegate() + { + var action = Substitute.For>(); + var (client, server) = await Initialize( + options => { }, + options => options + .OnDidChangeWorkspaceFolders(action, new object()) + ); + + var config = client.Services.GetRequiredService(); + config.Update("mysection", new Dictionary() { ["data"] = "value" }); + + client.WorkspaceFoldersManager.Add(new WorkspaceFolder() { Name = "foldera", Uri = "/some/path" }); + + await TestHelper.DelayUntil( + () => { + try + { + action.Received(1).Invoke(Arg.Any()); + return true; + } + catch (ReceivedCallsException e) + { + return false; + } + }, CancellationToken + ); + } + + [Fact] + public async Task Should_Disable_Configuration() + { + var action = Substitute.For>(); + var (_, server) = await Initialize( + options => { }, + options => options.OnDidChangeConfiguration(action, new object()) + ); + + var serverManager = server.Services.GetRequiredService(); + serverManager.Descriptors.Should().Contain(f => f.Handler is LanguageProtocolDelegatingHandlers.Notification); + serverManager.Descriptors.Should().ContainSingle(f => f.Method == WorkspaceNames.DidChangeConfiguration); + } + + [Fact] + public async Task Should_Allow_Custom_Configuration_Delegate() + { + var action = Substitute.For>(); + var (client, server) = await Initialize( + options => options + .WithCapability(new DidChangeConfigurationCapability() { DynamicRegistration = true }) + .WithServices(z => z.AddSingleton()), + options => options + .WithConfigurationSection("mysection") + .OnDidChangeConfiguration(action, new object()) + ); + + var clientManager = client.Services.GetRequiredService(); + clientManager.ContainsHandler(typeof(IConfigurationHandler)).Should().BeTrue(); + + var serverManager = server.Services.GetRequiredService(); + serverManager.ContainsHandler(typeof(IDidChangeConfigurationHandler)).Should().BeTrue(); + + var config = client.Services.GetRequiredService(); + config.Update("mysection", new Dictionary() { ["data"] = "value" }); + + await TestHelper.DelayUntil( + () => { + try + { + action.Received(1).Invoke(Arg.Is(z => Equals(z.Settings, JValue.CreateNull()))); + return true; + } + catch (ReceivedCallsException e) + { + return false; + } + }, CancellationToken + ); + } + } +} diff --git a/test/Lsp.Tests/Integration/LanguageServerConfigurationTests.cs b/test/Lsp.Tests/Integration/LanguageServerConfigurationTests.cs index 1a25ae43c..3ac29cc32 100644 --- a/test/Lsp.Tests/Integration/LanguageServerConfigurationTests.cs +++ b/test/Lsp.Tests/Integration/LanguageServerConfigurationTests.cs @@ -1,8 +1,13 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using FluentAssertions; using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; using NSubstitute; +using NSubstitute.Extensions; using OmniSharp.Extensions.JsonRpc.Testing; using OmniSharp.Extensions.LanguageProtocol.Testing; using OmniSharp.Extensions.LanguageServer.Client; @@ -161,6 +166,90 @@ public async Task Should_Only_Update_Configuration_Items_That_Are_Defined() server.Configuration["notmysection:key"].Should().BeNull(); } + [Fact] + public async Task Should_Support_Configuration_Binding() + { + var (_, server, configuration) = await InitializeWithConfiguration(ConfigureClient, ConfigureServer); + + configuration.Update("mysection", new Dictionary { ["host"] = "localhost", ["port"] = "443" }); + configuration.Update("notmysection", new Dictionary { ["host"] = "127.0.0.1", ["port"] = "123" }); + await TestHelper.DelayUntil(() => server.Configuration.AsEnumerable().Any(z => z.Key.StartsWith("mysection")), CancellationToken); + + var data = new BinderSourceUrl(); + server.Configuration.Bind("mysection", data); + data.Host.Should().Be("localhost"); + data.Port.Should().Be(443); + + configuration.Update("mysection", new Dictionary { ["host"] = "127.0.0.1", ["port"] = "80" }); + await server.Configuration.WaitForChange(CancellationToken); + await SettleNext(); + + data = new BinderSourceUrl(); + server.Configuration.Bind("mysection", data); + data.Host.Should().Be("127.0.0.1"); + data.Port.Should().Be(80); + } + + [Fact] + public async Task Should_Support_Options() + { + var (_, server, configuration) = await InitializeWithConfiguration(ConfigureClient, options => { + ConfigureServer(options); + options.Services.Configure("mysection"); + }); + + configuration.Update("mysection", new Dictionary { ["host"] = "localhost", ["port"] = "443" }); + configuration.Update("notmysection", new Dictionary { ["host"] = "127.0.0.1", ["port"] = "123" }); + await TestHelper.DelayUntil(() => server.Configuration.AsEnumerable().Any(z => z.Key.StartsWith("mysection")), CancellationToken); + + // + var options = server.GetService>(); + options.Value.Host.Should().Be("localhost"); + options.Value.Port.Should().Be(443); + + configuration.Update("mysection", new Dictionary { ["host"] = "127.0.0.1", ["port"] = "80" }); + await server.Configuration.WaitForChange(CancellationToken); + await SettleNext(); + + options.Value.Host.Should().Be("localhost"); + options.Value.Port.Should().Be(443); + } + + [Fact] + public async Task Should_Support_Options_Monitor() + { + var (_, server, configuration) = await InitializeWithConfiguration(ConfigureClient, options => { + ConfigureServer(options); + options.Services.Configure("mysection"); + }); + + var options = server.GetService>(); + var sub = Substitute.For>(); + options.OnChange(sub); + + configuration.Update("mysection", new Dictionary { ["host"] = "localhost", ["port"] = "443" }); + configuration.Update("notmysection", new Dictionary { ["host"] = "127.0.0.1", ["port"] = "123" }); + await options.WaitForChange(CancellationToken); + + // IOptionsMonitor<> is registered as a singleton, so this will update + options.CurrentValue.Host.Should().Be("localhost"); + options.CurrentValue.Port.Should().Be(443); + sub.Received(1).Invoke(Arg.Any()); + + configuration.Update("mysection", new Dictionary { ["host"] = "127.0.0.1", ["port"] = "80" }); + await options.WaitForChange(CancellationToken); + + options.CurrentValue.Host.Should().Be("127.0.0.1"); + options.CurrentValue.Port.Should().Be(80); + sub.Received(2).Invoke(Arg.Any()); + } + + class BinderSourceUrl + { + public string Host { get; set; } + public int Port { get; set; } + } + private void ConfigureClient(LanguageClientOptions options) { } diff --git a/test/Lsp.Tests/Lsp.Tests.csproj b/test/Lsp.Tests/Lsp.Tests.csproj index ec24f0d07..a903ade0f 100644 --- a/test/Lsp.Tests/Lsp.Tests.csproj +++ b/test/Lsp.Tests/Lsp.Tests.csproj @@ -15,8 +15,17 @@ + + + + + + + MSBuild:GenerateCodeFromAttributes + +