From 00dafaad0450f7f3ea2ea1b2799da093cfc0862e Mon Sep 17 00:00:00 2001 From: David Driscoll Date: Sat, 1 Aug 2020 00:50:15 -0400 Subject: [PATCH] Debug Adapter Server / Client update (#267) * Started work on DAP client and server * added missing properties, events, handlers and such. Everything compiles, but needs unit tests! * added unit tests around initialization, cancellation and progress * Added support for custom attach and launch request objectS * Updated module to allow extension data * Added support for derived Attach and Launch handlers for DAP * Added test to validate that handler collection supports handlers that implement more than one interface +semver:minor --- LSP.sln | 15 + src/Client/LanguageClient.cs | 14 +- src/Dap.Client/ClientProgressManager.cs | 65 +++++ src/Dap.Client/Dap.Client.csproj | 3 +- src/Dap.Client/DebugAdapterClient.cs | 198 +++++++++++++ src/Dap.Client/DebugAdapterClientOptions.cs | 86 ++++++ .../DebugAdapterClientOptionsExtensions.cs | 58 ++++ src/Dap.Client/ProgressObservable.cs | 44 +++ src/Dap.Protocol/Dap.Protocol.csproj | 25 +- src/Dap.Protocol/DapReceiver.cs | 100 +++++-- .../DapRpcErrorConverter.cs | 25 +- .../DebugAdapterRpcOptionsBase.cs | 14 + .../Events/CapabilitiesExtensions.cs | 4 +- src/Dap.Protocol/Events/EventNames.cs | 3 + .../Events/IProgressEndHandler.cs | 19 ++ .../Events/IProgressStartHandler.cs | 19 ++ .../Events/IProgressUpdateHandler.cs | 19 ++ src/Dap.Protocol/Events/ProcessEvent.cs | 2 +- src/Dap.Protocol/Events/ProgressEndEvent.cs | 12 + src/Dap.Protocol/Events/ProgressEvent.cs | 19 ++ src/Dap.Protocol/Events/ProgressStartEvent.cs | 37 +++ .../Events/ProgressUpdateEvent.cs | 17 ++ src/Dap.Protocol/Events/TerminatedEvent.cs | 6 +- src/Dap.Protocol/IClientProgressManager.cs | 9 + src/Dap.Protocol/IDebugAdapterClient.cs | 11 +- src/Dap.Protocol/IDebugAdapterServer.cs | 13 +- src/Dap.Protocol/IProgressObservable.cs | 9 + src/Dap.Protocol/IProgressObserver.cs | 12 + src/Dap.Protocol/IServerProgressManager.cs | 15 + src/Dap.Protocol/Models/Breakpoint.cs | 12 +- src/Dap.Protocol/Models/BreakpointLocation.cs | 30 ++ src/Dap.Protocol/Models/Capabilities.cs | 25 ++ src/Dap.Protocol/Models/ColumnDescriptor.cs | 4 +- src/Dap.Protocol/Models/CompletionItem.cs | 14 + src/Dap.Protocol/Models/DataBreakpoint.cs | 4 +- .../Models/InstructionBreakpoint.cs | 33 +++ src/Dap.Protocol/Models/Module.cs | 11 +- src/Dap.Protocol/Models/ProgressToken.cs | 93 ++++++ src/Dap.Protocol/Models/Source.cs | 2 +- src/Dap.Protocol/Models/StackFrame.cs | 2 +- .../Models/SteppingGranularity.cs | 13 + src/Dap.Protocol/OnClientStartedDelegate.cs | 8 + src/Dap.Protocol/OnServerStartedDelegate.cs | 8 + .../Requests/AttachRequestArguments.cs | 8 +- .../Requests/BreakpointLocationsArguments.cs | 38 +++ .../Requests/BreakpointLocationsResponse.cs | 11 + src/Dap.Protocol/Requests/CancelArguments.cs | 30 ++ src/Dap.Protocol/Requests/CancelResponse.cs | 22 ++ .../Requests/ContinueArguments.cs | 3 +- src/Dap.Protocol/Requests/IAttachHandler.cs | 18 +- .../Requests/IBreakpointLocationsHandler.cs | 18 ++ src/Dap.Protocol/Requests/ICancelHandler.cs | 22 ++ .../Requests/IDataBreakpointInfoHandler.cs | 22 +- .../Requests/IInitializeRequestArguments.cs | 66 +++++ src/Dap.Protocol/Requests/ILaunchHandler.cs | 17 +- .../Requests/IRestartFrameHandler.cs | 22 +- .../ISetInstructionBreakpointsHandler.cs | 18 ++ .../Requests/IVariablesHandler.cs | 22 +- .../Requests/InitializeRequestArguments.cs | 8 +- .../Requests/LaunchRequestArguments.cs | 6 + src/Dap.Protocol/Requests/NextArguments.cs | 9 +- src/Dap.Protocol/Requests/RequestNames.cs | 3 + .../Requests/RunInTerminalArguments.cs | 2 +- .../Requests/SetDataBreakpointsArguments.cs | 1 - .../Requests/SetDataBreakpointsResponse.cs | 1 - .../SetInstructionBreakpointsArguments.cs | 15 + .../SetInstructionBreakpointsResponse.cs | 12 + .../Requests/StepBackArguments.cs | 8 + src/Dap.Protocol/Requests/StepInArguments.cs | 7 + src/Dap.Protocol/Requests/StepOutArguments.cs | 8 + .../Requests/TerminateArguments.cs | 1 - .../Requests/VariablesArguments.cs | 2 +- .../Serialization/ContractResolver.cs | 2 +- src/Dap.Server/Dap.Server.csproj | 7 +- src/Dap.Server/DebugAdapterServer.cs | 266 +++++++++++++++++ src/Dap.Server/DebugAdapterServerOptions.cs | 106 +++++++ .../DebugAdapterServerOptionsExtensions.cs | 67 +++++ src/Dap.Server/InitializeDelegate.cs | 9 + src/Dap.Server/InitializedDelegate.cs | 9 + src/Dap.Server/ProgressObserver.cs | 83 ++++++ src/Dap.Server/ServerProgressManager.cs | 72 +++++ src/Dap.Shared/Dap.Shared.csproj | 21 ++ src/Dap.Shared/DapResponseRouter.cs | 146 ++++++++++ .../DebugAdapterHandlerCollection.cs | 166 +++++++++++ src/Dap.Shared/DebugAdapterRequestRouter.cs | 41 +++ src/Dap.Shared/HandlerDescriptor.cs | 62 ++++ src/Dap.Testing/LanguageProtocolTestBase.cs | 84 ++++++ src/Dap.Testing/LanguageServerTestBase.cs | 50 ++++ .../GenerateHandlerMethodsGenerator.cs | 44 ++- src/JsonRpc.Generators/Helpers.cs | 42 ++- src/JsonRpc.Testing/AggregateSettler.cs | 2 +- src/JsonRpc.Testing/JsonRpcTestBase.cs | 8 +- src/JsonRpc.Testing/SettlePipeline.cs | 2 +- src/JsonRpc.Testing/Settler.cs | 2 +- src/JsonRpc/ContentModified.cs | 4 +- .../GenerateHandlerMethodsAttribute.cs | 5 + src/JsonRpc/HandlerCollection.cs | 19 +- src/JsonRpc/HandlerTypeDescriptor.cs | 27 +- src/JsonRpc/HandlerTypeDescriptorHelper.cs | 19 +- src/JsonRpc/InputHandler.cs | 15 +- src/JsonRpc/JsonRpc.csproj | 3 + src/JsonRpc/MethodAttribute.cs | 25 ++ src/JsonRpc/Receiver.cs | 4 +- src/JsonRpc/RequestCancelled.cs | 4 +- src/JsonRpc/RequestRouterBase.cs | 2 +- src/JsonRpc/ResponseRouter.cs | 2 +- src/JsonRpc/RpcError.cs | 11 + src/JsonRpc/Server/Messages/InternalError.cs | 6 +- src/JsonRpc/Server/Messages/InvalidParams.cs | 4 +- src/JsonRpc/Server/Messages/InvalidRequest.cs | 8 +- src/JsonRpc/Server/Messages/MethodNotFound.cs | 2 +- src/JsonRpc/Server/Messages/ParseError.cs | 4 +- src/JsonRpc/Server/ServerErrorResult.cs | 3 +- src/Protocol/Models/Command.cs | 23 ++ .../Serialization/ContractResolver.cs | 2 +- src/Server/LspServerReceiver.cs | 2 +- src/Server/Messages/ServerNotInitialized.cs | 2 +- src/Server/Messages/UnknownErrorCode.cs | 2 +- src/Shared/RequestProcessIdentifier.cs | 2 +- test/Dap.Tests/DapOutputHandlerTests.cs | 2 +- .../DebugAdapterSpecifictionRecieverTests.cs | 27 +- test/Dap.Tests/FoundationTests.cs | 55 ++-- .../ConnectionAndDisconnectionTests.cs | 106 +++++++ .../Integration/CustomRequestsTests.cs | 267 ++++++++++++++++++ .../Integration/GenericDapServerTests.cs | 83 ++++++ test/Dap.Tests/Integration/ProgressTests.cs | 197 +++++++++++++ .../Integration/RequestCancellationTests.cs | 69 +++++ .../JsonRpcGenerationTests.cs | 115 +++++++- .../Server/SpecifictionRecieverTests.cs | 18 +- test/Lsp.Tests/FoundationTests.cs | 61 ++-- .../Integration/ExecuteCommandTests.cs | 104 ++----- .../Integration/RequestCancellationTests.cs | 12 +- .../Messages/ServerNotInitializedTests.cs | 2 +- .../Messages/UnknownErrorCodeTests.cs | 2 +- 134 files changed, 3783 insertions(+), 380 deletions(-) create mode 100644 src/Dap.Client/ClientProgressManager.cs create mode 100644 src/Dap.Client/DebugAdapterClient.cs create mode 100644 src/Dap.Client/DebugAdapterClientOptions.cs create mode 100644 src/Dap.Client/DebugAdapterClientOptionsExtensions.cs create mode 100644 src/Dap.Client/ProgressObservable.cs create mode 100644 src/Dap.Protocol/DebugAdapterRpcOptionsBase.cs create mode 100644 src/Dap.Protocol/Events/IProgressEndHandler.cs create mode 100644 src/Dap.Protocol/Events/IProgressStartHandler.cs create mode 100644 src/Dap.Protocol/Events/IProgressUpdateHandler.cs create mode 100644 src/Dap.Protocol/Events/ProgressEndEvent.cs create mode 100644 src/Dap.Protocol/Events/ProgressEvent.cs create mode 100644 src/Dap.Protocol/Events/ProgressStartEvent.cs create mode 100644 src/Dap.Protocol/Events/ProgressUpdateEvent.cs create mode 100644 src/Dap.Protocol/IClientProgressManager.cs create mode 100644 src/Dap.Protocol/IProgressObservable.cs create mode 100644 src/Dap.Protocol/IProgressObserver.cs create mode 100644 src/Dap.Protocol/IServerProgressManager.cs create mode 100644 src/Dap.Protocol/Models/BreakpointLocation.cs create mode 100644 src/Dap.Protocol/Models/InstructionBreakpoint.cs create mode 100644 src/Dap.Protocol/Models/ProgressToken.cs create mode 100644 src/Dap.Protocol/Models/SteppingGranularity.cs create mode 100644 src/Dap.Protocol/OnClientStartedDelegate.cs create mode 100644 src/Dap.Protocol/OnServerStartedDelegate.cs create mode 100644 src/Dap.Protocol/Requests/BreakpointLocationsArguments.cs create mode 100644 src/Dap.Protocol/Requests/BreakpointLocationsResponse.cs create mode 100644 src/Dap.Protocol/Requests/CancelArguments.cs create mode 100644 src/Dap.Protocol/Requests/CancelResponse.cs create mode 100644 src/Dap.Protocol/Requests/IBreakpointLocationsHandler.cs create mode 100644 src/Dap.Protocol/Requests/ICancelHandler.cs create mode 100644 src/Dap.Protocol/Requests/IInitializeRequestArguments.cs create mode 100644 src/Dap.Protocol/Requests/ISetInstructionBreakpointsHandler.cs create mode 100644 src/Dap.Protocol/Requests/SetInstructionBreakpointsArguments.cs create mode 100644 src/Dap.Protocol/Requests/SetInstructionBreakpointsResponse.cs create mode 100644 src/Dap.Server/DebugAdapterServer.cs create mode 100644 src/Dap.Server/DebugAdapterServerOptions.cs create mode 100644 src/Dap.Server/DebugAdapterServerOptionsExtensions.cs create mode 100644 src/Dap.Server/InitializeDelegate.cs create mode 100644 src/Dap.Server/InitializedDelegate.cs create mode 100644 src/Dap.Server/ProgressObserver.cs create mode 100644 src/Dap.Server/ServerProgressManager.cs create mode 100644 src/Dap.Shared/Dap.Shared.csproj create mode 100644 src/Dap.Shared/DapResponseRouter.cs create mode 100644 src/Dap.Shared/DebugAdapterHandlerCollection.cs create mode 100644 src/Dap.Shared/DebugAdapterRequestRouter.cs create mode 100644 src/Dap.Shared/HandlerDescriptor.cs create mode 100644 src/Dap.Testing/LanguageProtocolTestBase.cs create mode 100644 src/Dap.Testing/LanguageServerTestBase.cs create mode 100644 test/Dap.Tests/Integration/ConnectionAndDisconnectionTests.cs create mode 100644 test/Dap.Tests/Integration/CustomRequestsTests.cs create mode 100644 test/Dap.Tests/Integration/GenericDapServerTests.cs create mode 100644 test/Dap.Tests/Integration/ProgressTests.cs create mode 100644 test/Dap.Tests/Integration/RequestCancellationTests.cs diff --git a/LSP.sln b/LSP.sln index 3cc10f0f9..50b87a565 100644 --- a/LSP.sln +++ b/LSP.sln @@ -72,6 +72,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JsonRpc.Generators", "src\J EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Generation.Tests", "test\Generation.Tests\Generation.Tests.csproj", "{671FFF78-BDD2-4389-B29C-BFD183DA9120}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dap.Shared", "src\Dap.Shared\Dap.Shared.csproj", "{010D4BE7-6A92-4A04-B4EB-745FA3130DF2}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -316,6 +318,18 @@ Global {671FFF78-BDD2-4389-B29C-BFD183DA9120}.Release|x64.Build.0 = Release|Any CPU {671FFF78-BDD2-4389-B29C-BFD183DA9120}.Release|x86.ActiveCfg = Release|Any CPU {671FFF78-BDD2-4389-B29C-BFD183DA9120}.Release|x86.Build.0 = Release|Any CPU + {010D4BE7-6A92-4A04-B4EB-745FA3130DF2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {010D4BE7-6A92-4A04-B4EB-745FA3130DF2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {010D4BE7-6A92-4A04-B4EB-745FA3130DF2}.Debug|x64.ActiveCfg = Debug|Any CPU + {010D4BE7-6A92-4A04-B4EB-745FA3130DF2}.Debug|x64.Build.0 = Debug|Any CPU + {010D4BE7-6A92-4A04-B4EB-745FA3130DF2}.Debug|x86.ActiveCfg = Debug|Any CPU + {010D4BE7-6A92-4A04-B4EB-745FA3130DF2}.Debug|x86.Build.0 = Debug|Any CPU + {010D4BE7-6A92-4A04-B4EB-745FA3130DF2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {010D4BE7-6A92-4A04-B4EB-745FA3130DF2}.Release|Any CPU.Build.0 = Release|Any CPU + {010D4BE7-6A92-4A04-B4EB-745FA3130DF2}.Release|x64.ActiveCfg = Release|Any CPU + {010D4BE7-6A92-4A04-B4EB-745FA3130DF2}.Release|x64.Build.0 = Release|Any CPU + {010D4BE7-6A92-4A04-B4EB-745FA3130DF2}.Release|x86.ActiveCfg = Release|Any CPU + {010D4BE7-6A92-4A04-B4EB-745FA3130DF2}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -341,6 +355,7 @@ Global {202BA1AB-25DA-44ED-B962-FD82FCC74543} = {D764E024-3D3F-4112-B932-2DB722A1BACC} {DE259174-73DC-4532-B641-AD218971EE29} = {D764E024-3D3F-4112-B932-2DB722A1BACC} {671FFF78-BDD2-4389-B29C-BFD183DA9120} = {2F323ED5-EBF8-45E1-B9D3-C014561B3DDA} + {010D4BE7-6A92-4A04-B4EB-745FA3130DF2} = {D764E024-3D3F-4112-B932-2DB722A1BACC} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {D38DD0EC-D095-4BCD-B8AF-2D788AF3B9AE} diff --git a/src/Client/LanguageClient.cs b/src/Client/LanguageClient.cs index 292a10ccc..2f813c797 100644 --- a/src/Client/LanguageClient.cs +++ b/src/Client/LanguageClient.cs @@ -80,7 +80,7 @@ public static ILanguageClient PreInit(Action optionsActio public static async Task From(LanguageClientOptions options, CancellationToken token) { - var server = (LanguageClient)PreInit(options); + var server = (LanguageClient) PreInit(options); await server.Initialize(token); return server; @@ -238,6 +238,14 @@ public async Task Initialize(CancellationToken token) var serverParams = await this.RequestLanguageProtocolInitialize(ClientSettings, token); _receiver.Initialized(); + await _startedDelegates.Select(@delegate => + Observable.FromAsync(() => @delegate(this, serverParams, token)) + ) + .ToObservable() + .Merge() + .LastOrDefaultAsync() + .ToTask(token); + ServerSettings = serverParams; if (_collection.ContainsHandler(typeof(IRegisterCapabilityHandler))) RegistrationManager.RegisterCapabilities(serverParams.Capabilities); @@ -349,6 +357,7 @@ public void Dispose() object IServiceProvider.GetService(Type serviceType) => _serviceProvider.GetService(serviceType); protected override IResponseRouter ResponseRouter => _responseRouter; protected override IHandlersManager HandlersManager => _collection; + public IDisposable Register(Action registryAction) { var manager = new CompositeHandlersManager(_collection); @@ -359,7 +368,8 @@ public IDisposable Register(Action registryAction) class LangaugeClientRegistry : InterimLanguageProtocolRegistry, ILanguageClientRegistry { - public LangaugeClientRegistry(IServiceProvider serviceProvider, CompositeHandlersManager handlersManager, TextDocumentIdentifiers textDocumentIdentifiers) : base(serviceProvider, handlersManager, textDocumentIdentifiers) + public LangaugeClientRegistry(IServiceProvider serviceProvider, CompositeHandlersManager handlersManager, TextDocumentIdentifiers textDocumentIdentifiers) : base( + serviceProvider, handlersManager, textDocumentIdentifiers) { } } diff --git a/src/Dap.Client/ClientProgressManager.cs b/src/Dap.Client/ClientProgressManager.cs new file mode 100644 index 000000000..01380b4db --- /dev/null +++ b/src/Dap.Client/ClientProgressManager.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Reactive.Disposables; +using System.Reactive.Linq; +using System.Reactive.Subjects; +using System.Threading; +using System.Threading.Tasks; +using MediatR; +using OmniSharp.Extensions.DebugAdapter.Protocol.Events; +using OmniSharp.Extensions.DebugAdapter.Protocol.Models; + +namespace OmniSharp.Extensions.DebugAdapter.Client +{ + public class ClientProgressManager : IProgressStartHandler, IProgressUpdateHandler, IProgressEndHandler, IClientProgressManager, IDisposable + { + private readonly IObserver _observer; + private readonly CompositeDisposable _disposable = new CompositeDisposable(); + private readonly ConcurrentDictionary _activeObservables = new ConcurrentDictionary(EqualityComparer.Default); + + public ClientProgressManager() + { + var subject = new Subject(); + _disposable.Add(subject); + Progress = subject.AsObservable(); + _observer = subject; + } + + public IObservable Progress { get; } + + Task IRequestHandler.Handle(ProgressStartEvent request, CancellationToken cancellationToken) + { + var observable = new ProgressObservable(request.ProgressId); + _activeObservables.TryAdd(request.ProgressId, observable); + observable.OnNext(request); + _observer.OnNext(observable); + + return Unit.Task; + } + + Task IRequestHandler.Handle(ProgressUpdateEvent request, CancellationToken cancellationToken) + { + if (_activeObservables.TryGetValue(request.ProgressId, out var observable)) + { + observable.OnNext(request); + } + + // TODO: Add log message for unhandled? + return Unit.Task; + } + + Task IRequestHandler.Handle(ProgressEndEvent request, CancellationToken cancellationToken) + { + if (_activeObservables.TryGetValue(request.ProgressId, out var observable)) + { + observable.OnNext(request); + } + + // TODO: Add log message for unhandled? + return Unit.Task; + } + + public void Dispose() => _disposable?.Dispose(); + } +} diff --git a/src/Dap.Client/Dap.Client.csproj b/src/Dap.Client/Dap.Client.csproj index a3ddb4123..1eb4ee1ca 100644 --- a/src/Dap.Client/Dap.Client.csproj +++ b/src/Dap.Client/Dap.Client.csproj @@ -13,7 +13,6 @@ - - + diff --git a/src/Dap.Client/DebugAdapterClient.cs b/src/Dap.Client/DebugAdapterClient.cs new file mode 100644 index 000000000..fe564df0c --- /dev/null +++ b/src/Dap.Client/DebugAdapterClient.cs @@ -0,0 +1,198 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reactive.Disposables; +using System.Reactive.Linq; +using System.Reactive.Subjects; +using System.Reactive.Threading.Tasks; +using System.Threading; +using System.Threading.Tasks; +using MediatR; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using OmniSharp.Extensions.DebugAdapter.Protocol; +using OmniSharp.Extensions.DebugAdapter.Protocol.Events; +using OmniSharp.Extensions.DebugAdapter.Protocol.Requests; +using OmniSharp.Extensions.DebugAdapter.Shared; +using OmniSharp.Extensions.JsonRpc; +using IOutputHandler = OmniSharp.Extensions.JsonRpc.IOutputHandler; +using OutputHandler = OmniSharp.Extensions.JsonRpc.OutputHandler; + +namespace OmniSharp.Extensions.DebugAdapter.Client +{ + public class DebugAdapterClient : JsonRpcServerBase, IDebugAdapterClient, IInitializedHandler + { + private readonly DebugAdapterHandlerCollection _collection; + private readonly IEnumerable _startedDelegates; + private readonly CompositeDisposable _disposable = new CompositeDisposable(); + private readonly Connection _connection; + private readonly IClientProgressManager _progressManager; + private readonly DapReceiver _receiver; + private readonly ISubject _initializedComplete = new AsyncSubject(); + + public static Task From(Action optionsAction) + { + return From(optionsAction, CancellationToken.None); + } + + public static Task From(DebugAdapterClientOptions options) + { + return From(options, CancellationToken.None); + } + + public static Task From(Action optionsAction, CancellationToken token) + { + var options = new DebugAdapterClientOptions(); + optionsAction(options); + return From(options, token); + } + + public static IDebugAdapterClient PreInit(Action optionsAction) + { + var options = new DebugAdapterClientOptions(); + optionsAction(options); + return PreInit(options); + } + + public static async Task From(DebugAdapterClientOptions options, CancellationToken token) + { + var server = (DebugAdapterClient) PreInit(options); + await server.Initialize(token); + + return server; + } + + /// + /// Create the server without connecting to the client + /// + /// Mainly used for unit testing + /// + /// + /// + public static IDebugAdapterClient PreInit(DebugAdapterClientOptions options) + { + return new DebugAdapterClient(options); + } + + internal DebugAdapterClient(DebugAdapterClientOptions options) : base(options) + { + var services = options.Services; + services.AddLogging(builder => options.LoggingBuilderAction(builder)); + + ClientSettings = new InitializeRequestArguments() { + Locale = options.Locale, + AdapterId = options.AdapterId, + ClientId = options.ClientId, + ClientName = options.ClientName, + PathFormat = options.PathFormat, + ColumnsStartAt1 = options.ColumnsStartAt1, + LinesStartAt1 = options.LinesStartAt1, + SupportsMemoryReferences = options.SupportsMemoryReferences, + SupportsProgressReporting = options.SupportsProgressReporting, + SupportsVariablePaging = options.SupportsVariablePaging, + SupportsVariableType = options.SupportsVariableType, + SupportsRunInTerminalRequest = options.SupportsRunInTerminalRequest, + }; + + var serializer = options.Serializer; + var collection = new DebugAdapterHandlerCollection(); + services.AddSingleton(collection); + _collection = collection; + // _initializeDelegates = initializeDelegates; + // _initializedDelegates = initializedDelegates; + _startedDelegates = options.StartedDelegates; + + var receiver = _receiver = new DapReceiver(); + + services.AddSingleton(_ => + new OutputHandler(options.Output, options.Serializer, receiver.ShouldFilterOutput, _.GetService>())); + services.AddSingleton(_collection); + services.AddSingleton(serializer); + services.AddSingleton(options.RequestProcessIdentifier); + services.AddSingleton(receiver); + services.AddSingleton(this); + services.AddSingleton(); + services.AddSingleton>(_ => _.GetRequiredService()); + services.AddSingleton(); + + services.AddSingleton(); + services.AddSingleton(_ => _.GetRequiredService() as IJsonRpcHandler); + + EnsureAllHandlersAreRegistered(); + + var serviceProvider = services.BuildServiceProvider(); + _disposable.Add(serviceProvider); + IServiceProvider serviceProvider1 = serviceProvider; + + var responseRouter = serviceProvider1.GetRequiredService(); + ResponseRouter = responseRouter; + _progressManager = serviceProvider1.GetRequiredService(); + + _connection = new Connection( + options.Input, + serviceProvider1.GetRequiredService(), + receiver, + options.RequestProcessIdentifier, + serviceProvider1.GetRequiredService>(), + responseRouter, + serviceProvider1.GetRequiredService(), + options.OnUnhandledException ?? (e => { }), + options.CreateResponseException, + options.MaximumRequestTimeout, + false, + options.Concurrency + ); + + var serviceHandlers = serviceProvider1.GetServices().ToArray(); + _collection.Add(this); + _disposable.Add(_collection.Add(serviceHandlers)); + options.AddLinks(_collection); + } + + public async Task Initialize(CancellationToken token) + { + RegisterCapabilities(ClientSettings); + + _connection.Open(); + var serverParams = await this.RequestInitialize(ClientSettings, token); + + ServerSettings = serverParams; + _receiver.Initialized(); + + await _initializedComplete.ToTask(token); + + await _startedDelegates.Select(@delegate => Observable.FromAsync(() => @delegate(this, serverParams, token))) + .ToObservable() + .Merge() + .LastOrDefaultAsync() + .ToTask(token); + } + + Task IRequestHandler.Handle(InitializedEvent request, CancellationToken cancellationToken) + { + _initializedComplete.OnNext(request); + _initializedComplete.OnCompleted(); + return Unit.Task; + } + + private void RegisterCapabilities(InitializeRequestArguments capabilities) + { + capabilities.SupportsRunInTerminalRequest ??= _collection.ContainsHandler(typeof(IRunInTerminalHandler)); + capabilities.SupportsProgressReporting ??= _collection.ContainsHandler(typeof(IProgressStartHandler)) && + _collection.ContainsHandler(typeof(IProgressUpdateHandler)) && + _collection.ContainsHandler(typeof(IProgressEndHandler)); + } + + protected override IResponseRouter ResponseRouter { get; } + protected override IHandlersManager HandlersManager => _collection; + public InitializeRequestArguments ClientSettings { get; } + public InitializeResponse ServerSettings { get; private set; } + public IClientProgressManager ProgressManager => _progressManager; + + public void Dispose() + { + _disposable?.Dispose(); + _connection?.Dispose(); + } + } +} diff --git a/src/Dap.Client/DebugAdapterClientOptions.cs b/src/Dap.Client/DebugAdapterClientOptions.cs new file mode 100644 index 000000000..3711a81f3 --- /dev/null +++ b/src/Dap.Client/DebugAdapterClientOptions.cs @@ -0,0 +1,86 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Newtonsoft.Json.Linq; +using OmniSharp.Extensions.DebugAdapter.Protocol; +using OmniSharp.Extensions.DebugAdapter.Protocol.Requests; +using OmniSharp.Extensions.JsonRpc; + +namespace OmniSharp.Extensions.DebugAdapter.Client +{ + public class DebugAdapterClientOptions : DebugAdapterRpcOptionsBase, IDebugAdapterClientRegistry, IInitializeRequestArguments + { + internal readonly List StartedDelegates = new List(); + public ISerializer Serializer { get; set; } = new DapSerializer(); + public override IRequestProcessIdentifier RequestProcessIdentifier { get; set; } = new ParallelRequestProcessIdentifier(); + public string ClientId { get; set; } + public string ClientName { get; set; } + public string AdapterId { get; set; } + public string Locale { get; set; } + public bool? LinesStartAt1 { get; set; } + public bool? ColumnsStartAt1 { get; set; } + public string PathFormat { get; set; } + public bool? SupportsVariableType { get; set; } + public bool? SupportsVariablePaging { get; set; } + public bool? SupportsRunInTerminalRequest { get; set; } + public bool? SupportsMemoryReferences { get; set; } + public bool? SupportsProgressReporting { get; set; } + IDebugAdapterClientRegistry IJsonRpcHandlerRegistry.AddHandler(string method, IJsonRpcHandler handler, JsonRpcHandlerOptions options) => this.AddHandler(method, handler, options); + + IDebugAdapterClientRegistry IJsonRpcHandlerRegistry.AddHandler(string method, Func handlerFunc, JsonRpcHandlerOptions options) => this.AddHandler(method, handlerFunc, options); + + IDebugAdapterClientRegistry IJsonRpcHandlerRegistry.AddHandlers(params IJsonRpcHandler[] handlers) => this.AddHandlers(handlers); + + IDebugAdapterClientRegistry IJsonRpcHandlerRegistry.AddHandler(Func handlerFunc, JsonRpcHandlerOptions options) => this.AddHandler(handlerFunc, options); + + IDebugAdapterClientRegistry IJsonRpcHandlerRegistry.AddHandler(THandler handler, JsonRpcHandlerOptions options) => this.AddHandler(handler, options); + + IDebugAdapterClientRegistry IJsonRpcHandlerRegistry.AddHandler(JsonRpcHandlerOptions options) => this.AddHandler(options); + + IDebugAdapterClientRegistry IJsonRpcHandlerRegistry.AddHandler(string method, JsonRpcHandlerOptions options) => this.AddHandler(method, options); + + IDebugAdapterClientRegistry IJsonRpcHandlerRegistry.AddHandler(Type type, JsonRpcHandlerOptions options) => this.AddHandler(type, options); + + IDebugAdapterClientRegistry IJsonRpcHandlerRegistry.AddHandler(string method, Type type, JsonRpcHandlerOptions options) => this.AddHandler(method, type, options); + + IDebugAdapterClientRegistry IJsonRpcHandlerRegistry.OnJsonRequest(string method, Func> handler, JsonRpcHandlerOptions options) => OnJsonRequest(method, handler, options); + + IDebugAdapterClientRegistry IJsonRpcHandlerRegistry.OnJsonRequest(string method, Func> handler, JsonRpcHandlerOptions options) => OnJsonRequest(method, handler, options); + IDebugAdapterClientRegistry IJsonRpcHandlerRegistry.OnRequest(string method, Func> handler, JsonRpcHandlerOptions options) => OnRequest(method, handler, options); + + IDebugAdapterClientRegistry IJsonRpcHandlerRegistry.OnRequest(string method, Func> handler, JsonRpcHandlerOptions options) => OnRequest(method, handler, options); + + IDebugAdapterClientRegistry IJsonRpcHandlerRegistry.OnRequest(string method, Func> handler, JsonRpcHandlerOptions options) => OnRequest(method, handler, options); + + IDebugAdapterClientRegistry IJsonRpcHandlerRegistry.OnRequest(string method, Func> handler, JsonRpcHandlerOptions options) => OnRequest(method, handler, options); + + IDebugAdapterClientRegistry IJsonRpcHandlerRegistry.OnRequest(string method, Func handler, JsonRpcHandlerOptions options) => OnRequest(method, handler, options); + + IDebugAdapterClientRegistry IJsonRpcHandlerRegistry.OnRequest(string method, Func handler, JsonRpcHandlerOptions options) => OnRequest(method, handler, options); + + IDebugAdapterClientRegistry IJsonRpcHandlerRegistry.OnRequest(string method, Func handler, JsonRpcHandlerOptions options) => OnRequest(method, handler, options); + + IDebugAdapterClientRegistry IJsonRpcHandlerRegistry.OnNotification(string method, Action handler, JsonRpcHandlerOptions options) => OnNotification(method, handler, options); + + IDebugAdapterClientRegistry IJsonRpcHandlerRegistry.OnJsonNotification(string method, Action handler, JsonRpcHandlerOptions options) => OnJsonNotification(method, handler, options); + + IDebugAdapterClientRegistry IJsonRpcHandlerRegistry.OnJsonNotification(string method, Func handler, JsonRpcHandlerOptions options) => OnJsonNotification(method, handler, options); + + IDebugAdapterClientRegistry IJsonRpcHandlerRegistry.OnJsonNotification(string method, Func handler, JsonRpcHandlerOptions options) => OnJsonNotification(method, handler, options); + + IDebugAdapterClientRegistry IJsonRpcHandlerRegistry.OnJsonNotification(string method, Action handler, JsonRpcHandlerOptions options) => OnJsonNotification(method, handler, options); + + IDebugAdapterClientRegistry IJsonRpcHandlerRegistry.OnNotification(string method, Action handler, JsonRpcHandlerOptions options) => OnNotification(method, handler, options); + + IDebugAdapterClientRegistry IJsonRpcHandlerRegistry.OnNotification(string method, Func handler, JsonRpcHandlerOptions options) => OnNotification(method, handler, options); + + IDebugAdapterClientRegistry IJsonRpcHandlerRegistry.OnNotification(string method, Func handler, JsonRpcHandlerOptions options) => OnNotification(method, handler, options); + + IDebugAdapterClientRegistry IJsonRpcHandlerRegistry.OnNotification(string method, Action handler, JsonRpcHandlerOptions options) => OnNotification(method, handler, options); + + IDebugAdapterClientRegistry IJsonRpcHandlerRegistry.OnNotification(string method, Func handler, JsonRpcHandlerOptions options) => OnNotification(method, handler, options); + + IDebugAdapterClientRegistry IJsonRpcHandlerRegistry.OnNotification(string method, Func handler, JsonRpcHandlerOptions options) => OnNotification(method, handler, options); + } +} \ No newline at end of file diff --git a/src/Dap.Client/DebugAdapterClientOptionsExtensions.cs b/src/Dap.Client/DebugAdapterClientOptionsExtensions.cs new file mode 100644 index 000000000..b466d8736 --- /dev/null +++ b/src/Dap.Client/DebugAdapterClientOptionsExtensions.cs @@ -0,0 +1,58 @@ +using System; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using OmniSharp.Extensions.DebugAdapter.Protocol; +using OmniSharp.Extensions.JsonRpc; + +namespace OmniSharp.Extensions.DebugAdapter.Client +{ + public static class DebugAdapterClientOptionsExtensions + { + + public static DebugAdapterClientOptions WithSerializer(this DebugAdapterClientOptions options, ISerializer serializer) + { + options.Serializer = serializer; + return options; + } + + public static DebugAdapterClientOptions WithRequestProcessIdentifier(this DebugAdapterClientOptions options, IRequestProcessIdentifier requestProcessIdentifier) + { + options.RequestProcessIdentifier = requestProcessIdentifier; + return options; + } + + public static DebugAdapterClientOptions WithServices(this DebugAdapterClientOptions options, Action servicesAction) + { + servicesAction(options.Services); + return options; + } + + public static DebugAdapterClientOptions OnStarted(this DebugAdapterClientOptions options, + OnClientStartedDelegate @delegate) + { + options.StartedDelegates.Add(@delegate); + return options; + } + + public static DebugAdapterClientOptions ConfigureLogging(this DebugAdapterClientOptions options, + Action builderAction) + { + options.LoggingBuilderAction = builderAction; + return options; + } + + public static DebugAdapterClientOptions AddDefaultLoggingProvider(this DebugAdapterClientOptions options) + { + options.AddDefaultLoggingProvider = true; + return options; + } + + public static DebugAdapterClientOptions ConfigureConfiguration(this DebugAdapterClientOptions options, + Action builderAction) + { + options.ConfigurationBuilderAction = builderAction; + return options; + } + } +} \ No newline at end of file diff --git a/src/Dap.Client/ProgressObservable.cs b/src/Dap.Client/ProgressObservable.cs new file mode 100644 index 000000000..be05cecce --- /dev/null +++ b/src/Dap.Client/ProgressObservable.cs @@ -0,0 +1,44 @@ +using System; +using System.Reactive.Disposables; +using System.Reactive.Subjects; +using OmniSharp.Extensions.DebugAdapter.Protocol.Events; +using OmniSharp.Extensions.DebugAdapter.Protocol.Models; + +namespace OmniSharp.Extensions.DebugAdapter.Client +{ + class ProgressObservable : IProgressObservable, IObserver, IDisposable + { + private readonly CompositeDisposable _disposable; + private readonly ReplaySubject _dataSubject; + + public ProgressObservable(ProgressToken token) + { + _dataSubject = new ReplaySubject(1); + _disposable = new CompositeDisposable() {Disposable.Create(_dataSubject.OnCompleted)}; + + ProgressToken = token; + if (_dataSubject is IDisposable disposable) + { + _disposable.Add(disposable); + } + } + + public ProgressToken ProgressToken { get; } + + void IObserver.OnCompleted() => _dataSubject.OnCompleted(); + + void IObserver.OnError(Exception error) => _dataSubject.OnError(error); + + public void OnNext(ProgressEvent value) => _dataSubject.OnNext(value); + + public void Dispose() + { + _disposable.Dispose(); + } + + public IDisposable Subscribe(IObserver observer) + { + return _disposable.IsDisposed ? Disposable.Empty : _dataSubject.Subscribe(observer); + } + } +} \ No newline at end of file diff --git a/src/Dap.Protocol/Dap.Protocol.csproj b/src/Dap.Protocol/Dap.Protocol.csproj index d25616d5f..c557f7163 100644 --- a/src/Dap.Protocol/Dap.Protocol.csproj +++ b/src/Dap.Protocol/Dap.Protocol.csproj @@ -13,11 +13,32 @@ - <_Parameter1>OmniSharp.Extensions.LanguageServer, PublicKey=0024000004800000940000000602000000240000525341310004000001000100391db875e68eb4bfef49ce14313b9e13f2cd3cc89eb273bbe6c11a55044c7d4f566cf092e1c77ef9e7c75b1496ae7f95d925938f5a01793dd8d9f99ae0a7595779b71b971287d7d7b5960d052078d14f5ce1a85ea5c9fb2f59ac735ff7bc215cab469b7c3486006860bad6f4c3b5204ea2f28dd4e1d05e2cca462cfd593b9f9f + <_Parameter1>OmniSharp.Extensions.DebugAdapter.Server, PublicKey=0024000004800000940000000602000000240000525341310004000001000100391db875e68eb4bfef49ce14313b9e13f2cd3cc89eb273bbe6c11a55044c7d4f566cf092e1c77ef9e7c75b1496ae7f95d925938f5a01793dd8d9f99ae0a7595779b71b971287d7d7b5960d052078d14f5ce1a85ea5c9fb2f59ac735ff7bc215cab469b7c3486006860bad6f4c3b5204ea2f28dd4e1d05e2cca462cfd593b9f9f - <_Parameter1>OmniSharp.Extensions.LanguageClient, PublicKey=0024000004800000940000000602000000240000525341310004000001000100391db875e68eb4bfef49ce14313b9e13f2cd3cc89eb273bbe6c11a55044c7d4f566cf092e1c77ef9e7c75b1496ae7f95d925938f5a01793dd8d9f99ae0a7595779b71b971287d7d7b5960d052078d14f5ce1a85ea5c9fb2f59ac735ff7bc215cab469b7c3486006860bad6f4c3b5204ea2f28dd4e1d05e2cca462cfd593b9f9f + <_Parameter1>OmniSharp.Extensions.DebugAdapter.Client, PublicKey=0024000004800000940000000602000000240000525341310004000001000100391db875e68eb4bfef49ce14313b9e13f2cd3cc89eb273bbe6c11a55044c7d4f566cf092e1c77ef9e7c75b1496ae7f95d925938f5a01793dd8d9f99ae0a7595779b71b971287d7d7b5960d052078d14f5ce1a85ea5c9fb2f59ac735ff7bc215cab469b7c3486006860bad6f4c3b5204ea2f28dd4e1d05e2cca462cfd593b9f9f + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Dap.Protocol/DapReceiver.cs b/src/Dap.Protocol/DapReceiver.cs index e0dc0b85f..ef1f42328 100644 --- a/src/Dap.Protocol/DapReceiver.cs +++ b/src/Dap.Protocol/DapReceiver.cs @@ -1,17 +1,33 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; +using System.Linq; +using System.Reactive.Disposables; +using System.Reactive.Linq; +using System.Reactive.Subjects; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using MediatR; using Newtonsoft.Json.Linq; +using OmniSharp.Extensions.DebugAdapter.Protocol.Events; +using OmniSharp.Extensions.DebugAdapter.Protocol.Models; +using OmniSharp.Extensions.DebugAdapter.Protocol.Requests; +using OmniSharp.Extensions.JsonRpc; +using OmniSharp.Extensions.JsonRpc.Client; using OmniSharp.Extensions.JsonRpc.Server; using OmniSharp.Extensions.JsonRpc.Server.Messages; -namespace OmniSharp.Extensions.JsonRpc +namespace OmniSharp.Extensions.DebugAdapter.Protocol { public class DapReceiver : IReceiver { + private bool _initialized; + public (IEnumerable results, bool hasResponse) GetRequests(JToken container) { - var result = GetRenor(container); - return (new[] { result }, result.IsResponse); + var result = GetRenor(container).ToArray(); + return (result, result.Any(z => z.IsResponse)); } public bool IsValid(JToken container) @@ -24,22 +40,26 @@ public bool IsValid(JToken container) return false; } - protected virtual Renor GetRenor(JToken @object) + protected virtual IEnumerable GetRenor(JToken @object) { - if (!( @object is JObject request )) + if (!(@object is JObject request)) { - return new InvalidRequest(null, "Not an object"); + yield return new InvalidRequest(null, "Not an object"); + yield break; } if (!request.TryGetValue("seq", out var id)) { - return new InvalidRequest(null, "No sequence given"); + yield return new InvalidRequest(null, "No sequence given"); + yield break; } if (!request.TryGetValue("type", out var type)) { - return new InvalidRequest(null, "No type given"); + yield return new InvalidRequest(null, "No type given"); + yield break; } + var sequence = id.Value(); var messageType = type.Value(); @@ -47,31 +67,61 @@ protected virtual Renor GetRenor(JToken @object) { if (!request.TryGetValue("event", out var @event)) { - return new InvalidRequest(null, "No event given"); + yield return new InvalidRequest(null, "No event given"); + yield break; } - return new Notification(@event.Value(), request.TryGetValue("body", out var body) ? body : null); + + yield return new Notification(@event.Value(), request.TryGetValue("body", out var body) ? body : null); + yield break; } + if (messageType == "request") { if (!request.TryGetValue("command", out var command)) { - return new InvalidRequest(null, "No command given"); + yield return new InvalidRequest(null, "No command given"); + yield break; + } + + var requestName = command.Value(); + var requestObject = request.TryGetValue("arguments", out var body) ? body : new JObject(); + if (RequestNames.Cancel == requestName && requestObject is JObject ro) + { + // DAP is really weird... the cancellation operation mixes request and progress cancellation. + // because we already have the assumption of the cancellation token we are going to just split the request up. + // This makes it so that the cancel handler implementer must still return a positive response even if the request didn't make it through. + if (ro.TryGetValue("requestId", out var requestId)) + { + yield return new Notification(JsonRpcNames.CancelRequest, JObject.FromObject(new {id = requestId})); + ro.Remove("requestId"); + } + + yield return new Request(sequence, RequestNames.Cancel, ro); + yield break; } - return new Request(sequence, command.Value(), request.TryGetValue("arguments", out var body) ? body : new JObject()); + + yield return new Request(sequence, requestName, requestObject); + yield break; } + if (messageType == "response") { if (!request.TryGetValue("request_seq", out var request_seq)) { - return new InvalidRequest(null, "No request_seq given"); + yield return new InvalidRequest(null, "No request_seq given"); + yield break; } + if (!request.TryGetValue("command", out var command)) { - return new InvalidRequest(null, "No command given"); + yield return new InvalidRequest(null, "No command given"); + yield break; } + if (!request.TryGetValue("success", out var success)) { - return new InvalidRequest(null, "No success given"); + yield return new InvalidRequest(null, "No success given"); + yield break; } var bodyValue = request.TryGetValue("body", out var body) ? body : null; @@ -81,12 +131,28 @@ protected virtual Renor GetRenor(JToken @object) if (successValue) { - return new ServerResponse(requestSequence, bodyValue); + yield return new ServerResponse(requestSequence, bodyValue); + yield break; } - return new ServerError(requestSequence, bodyValue.ToObject()); + + yield return new ServerError(requestSequence, bodyValue?.ToObject() ?? new ServerErrorResult(-1, "Unknown Error")); + yield break; } throw new NotSupportedException($"Message type {messageType} is not supported"); } + + public void Initialized() + { + _initialized = true; + } + + public bool ShouldFilterOutput(object value) + { + if (_initialized) return true; + return value is OutgoingResponse || + (value is OutgoingNotification n && (n.Params is InitializedEvent)) || + (value is OutgoingRequest r && r.Params is InitializeRequestArguments); + } } } diff --git a/src/Dap.Protocol/DebugAdapterConverters/DapRpcErrorConverter.cs b/src/Dap.Protocol/DebugAdapterConverters/DapRpcErrorConverter.cs index ab29136d2..2aab3267b 100644 --- a/src/Dap.Protocol/DebugAdapterConverters/DapRpcErrorConverter.cs +++ b/src/Dap.Protocol/DebugAdapterConverters/DapRpcErrorConverter.cs @@ -6,7 +6,7 @@ namespace OmniSharp.Extensions.DebugAdapter.Protocol.DebugAdapterConverters { - public class DapRpcErrorConverter : JsonConverter + public class DapRpcErrorConverter : JsonConverter { private readonly ISerializer _serializer; @@ -15,29 +15,36 @@ public DapRpcErrorConverter(ISerializer serializer) _serializer = serializer; } - public override void WriteJson(JsonWriter writer, RpcError value, JsonSerializer serializer) + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { + if (!(value is RpcError error)) + { + throw new NotSupportedException($"{typeof(RpcError).FullName} was not found!"); + } + writer.WriteStartObject(); writer.WritePropertyName("seq"); writer.WriteValue(_serializer.GetNextId()); writer.WritePropertyName("type"); writer.WriteValue("response"); - if (value.Id != null) + if (error.Id != null) { writer.WritePropertyName("request_seq"); - writer.WriteValue(value.Id); + writer.WriteValue(error.Id); } writer.WritePropertyName("success"); writer.WriteValue(false); + writer.WritePropertyName("command"); + writer.WriteValue(error.Method); writer.WritePropertyName("message"); - writer.WriteValue(value.Error.Data); + writer.WriteValue(error.Error.Message); + writer.WritePropertyName("body"); + serializer.Serialize(writer, error.Error); writer.WriteEndObject(); } - public override RpcError ReadJson(JsonReader reader, Type objectType, RpcError existingValue, - bool hasExistingValue, JsonSerializer serializer) + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { - var obj = JObject.Load(reader); object requestId = null; @@ -57,5 +64,7 @@ public override RpcError ReadJson(JsonReader reader, Type objectType, RpcError e return new RpcError(requestId, data); } + + public override bool CanConvert(Type objectType) => typeof(RpcError).IsAssignableFrom(objectType); } } diff --git a/src/Dap.Protocol/DebugAdapterRpcOptionsBase.cs b/src/Dap.Protocol/DebugAdapterRpcOptionsBase.cs new file mode 100644 index 000000000..64890fe92 --- /dev/null +++ b/src/Dap.Protocol/DebugAdapterRpcOptionsBase.cs @@ -0,0 +1,14 @@ +using System; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using OmniSharp.Extensions.JsonRpc; + +namespace OmniSharp.Extensions.DebugAdapter.Protocol +{ + public abstract class DebugAdapterRpcOptionsBase : JsonRpcServerOptionsBase where T : IJsonRpcHandlerRegistry + { + internal bool AddDefaultLoggingProvider { get; set; } + internal Action LoggingBuilderAction { get; set; } = _ => { }; + internal Action ConfigurationBuilderAction { get; set; } = _ => { }; + } +} \ No newline at end of file diff --git a/src/Dap.Protocol/Events/CapabilitiesExtensions.cs b/src/Dap.Protocol/Events/CapabilitiesExtensions.cs index 195d8c515..056635252 100644 --- a/src/Dap.Protocol/Events/CapabilitiesExtensions.cs +++ b/src/Dap.Protocol/Events/CapabilitiesExtensions.cs @@ -8,7 +8,9 @@ namespace OmniSharp.Extensions.DebugAdapter.Protocol.Events { [Parallel, Method(EventNames.Capabilities, Direction.ServerToClient)] [GenerateRequestMethods, GenerateHandlerMethods] - public interface ICapabilitiesHandler : IJsonRpcNotificationHandler { } + public interface ICapabilitiesHandler : IJsonRpcNotificationHandler + { + } public abstract class CapabilitiesHandler : ICapabilitiesHandler { diff --git a/src/Dap.Protocol/Events/EventNames.cs b/src/Dap.Protocol/Events/EventNames.cs index 6ccac273a..0edf45262 100644 --- a/src/Dap.Protocol/Events/EventNames.cs +++ b/src/Dap.Protocol/Events/EventNames.cs @@ -14,5 +14,8 @@ public static class EventNames public const string LoadedSource = "loadedSource"; public const string Process = "process"; public const string Capabilities = "capabilities"; + public const string ProgressStart = "progressStart"; + public const string ProgressUpdate = "progressUpdate"; + public const string ProgressEnd = "progressEnd"; } } diff --git a/src/Dap.Protocol/Events/IProgressEndHandler.cs b/src/Dap.Protocol/Events/IProgressEndHandler.cs new file mode 100644 index 000000000..f7481bdcc --- /dev/null +++ b/src/Dap.Protocol/Events/IProgressEndHandler.cs @@ -0,0 +1,19 @@ +using System.Threading; +using System.Threading.Tasks; +using MediatR; +using OmniSharp.Extensions.JsonRpc; +using OmniSharp.Extensions.JsonRpc.Generation; + +namespace OmniSharp.Extensions.DebugAdapter.Protocol.Events +{ + [Parallel, Method(EventNames.ProgressEnd, Direction.ServerToClient)] + [GenerateHandlerMethods, GenerateRequestMethods] + public interface IProgressEndHandler : IJsonRpcNotificationHandler + { + } + + public abstract class ProgressEndHandlerBase : IProgressEndHandler + { + public abstract Task Handle(ProgressEndEvent request, CancellationToken cancellationToken); + } +} diff --git a/src/Dap.Protocol/Events/IProgressStartHandler.cs b/src/Dap.Protocol/Events/IProgressStartHandler.cs new file mode 100644 index 000000000..3695bfc38 --- /dev/null +++ b/src/Dap.Protocol/Events/IProgressStartHandler.cs @@ -0,0 +1,19 @@ +using System.Threading; +using System.Threading.Tasks; +using MediatR; +using OmniSharp.Extensions.JsonRpc; +using OmniSharp.Extensions.JsonRpc.Generation; + +namespace OmniSharp.Extensions.DebugAdapter.Protocol.Events +{ + [Parallel, Method(EventNames.ProgressStart, Direction.ServerToClient)] + [GenerateHandlerMethods, GenerateRequestMethods] + public interface IProgressStartHandler : IJsonRpcNotificationHandler + { + } + + public abstract class ProgressStartHandlerBase : IProgressStartHandler + { + public abstract Task Handle(ProgressStartEvent request, CancellationToken cancellationToken); + } +} diff --git a/src/Dap.Protocol/Events/IProgressUpdateHandler.cs b/src/Dap.Protocol/Events/IProgressUpdateHandler.cs new file mode 100644 index 000000000..418bf5348 --- /dev/null +++ b/src/Dap.Protocol/Events/IProgressUpdateHandler.cs @@ -0,0 +1,19 @@ +using System.Threading; +using System.Threading.Tasks; +using MediatR; +using OmniSharp.Extensions.JsonRpc; +using OmniSharp.Extensions.JsonRpc.Generation; + +namespace OmniSharp.Extensions.DebugAdapter.Protocol.Events +{ + [Parallel, Method(EventNames.ProgressUpdate, Direction.ServerToClient)] + [GenerateHandlerMethods, GenerateRequestMethods] + public interface IProgressUpdateHandler : IJsonRpcNotificationHandler + { + } + + public abstract class ProgressUpdateHandlerBase : IProgressUpdateHandler + { + public abstract Task Handle(ProgressUpdateEvent request, CancellationToken cancellationToken); + } +} diff --git a/src/Dap.Protocol/Events/ProcessEvent.cs b/src/Dap.Protocol/Events/ProcessEvent.cs index 5707ee33d..9458b6bf2 100644 --- a/src/Dap.Protocol/Events/ProcessEvent.cs +++ b/src/Dap.Protocol/Events/ProcessEvent.cs @@ -29,7 +29,7 @@ public class ProcessEvent : IRequest /// 'attach': Debugger attached to an existing process. /// 'attachForSuspendedLaunch': A project launcher component has launched a new process in a suspended state and then asked the debugger to attach. /// - [Optional] public ProcessEventStartMethod StartMethod { get; set; } + [Optional] public ProcessEventStartMethod? StartMethod { get; set; } /// /// The size of a pointer or address for this process, in bits. This value may be used by clients when formatting addresses for display. diff --git a/src/Dap.Protocol/Events/ProgressEndEvent.cs b/src/Dap.Protocol/Events/ProgressEndEvent.cs new file mode 100644 index 000000000..6268be688 --- /dev/null +++ b/src/Dap.Protocol/Events/ProgressEndEvent.cs @@ -0,0 +1,12 @@ +using MediatR; +using OmniSharp.Extensions.DebugAdapter.Protocol.Models; +using OmniSharp.Extensions.DebugAdapter.Protocol.Serialization; +using OmniSharp.Extensions.JsonRpc; + +namespace OmniSharp.Extensions.DebugAdapter.Protocol.Events +{ + [Method(EventNames.ProgressEnd, Direction.ServerToClient)] + public class ProgressEndEvent : ProgressEvent, IRequest + { + } +} diff --git a/src/Dap.Protocol/Events/ProgressEvent.cs b/src/Dap.Protocol/Events/ProgressEvent.cs new file mode 100644 index 000000000..5752cbb68 --- /dev/null +++ b/src/Dap.Protocol/Events/ProgressEvent.cs @@ -0,0 +1,19 @@ +using OmniSharp.Extensions.DebugAdapter.Protocol.Models; +using OmniSharp.Extensions.DebugAdapter.Protocol.Serialization; + +namespace OmniSharp.Extensions.DebugAdapter.Protocol.Events +{ + public abstract class ProgressEvent + { + /// + /// The ID that was introduced in the initial 'progressStart' event. + /// + public ProgressToken ProgressId { get; set; } + + /// + /// Optional, more detailed progress message. If omitted, the previous message (if any) is used. + /// + [Optional] + public string Message { get; set; } + } +} diff --git a/src/Dap.Protocol/Events/ProgressStartEvent.cs b/src/Dap.Protocol/Events/ProgressStartEvent.cs new file mode 100644 index 000000000..8d3d623eb --- /dev/null +++ b/src/Dap.Protocol/Events/ProgressStartEvent.cs @@ -0,0 +1,37 @@ +using MediatR; +using OmniSharp.Extensions.DebugAdapter.Protocol.Serialization; +using OmniSharp.Extensions.JsonRpc; + +namespace OmniSharp.Extensions.DebugAdapter.Protocol.Events +{ + [Method(EventNames.ProgressStart, Direction.ServerToClient)] + public class ProgressStartEvent : ProgressEvent, IRequest + { + /// + /// Mandatory (short) title of the progress reporting. Shown in the UI to describe the long running operation. + /// + public string Title { get; set; } + + /// + /// The request ID that this progress report is related to. If specified a debug adapter is expected to emit + /// progress events for the long running request until the request has been either completed or cancelled. + /// If the request ID is omitted, the progress report is assumed to be related to some general activity of the debug adapter. + /// + [Optional] + public int RequestId { get; set; } + + /// + /// If true, the request that reports progress may be canceled with a 'cancel' request. + /// So this property basically controls whether the client should use UX that supports cancellation. + /// Clients that don't support cancellation are allowed to ignore the setting. + /// + [Optional] + public bool? Cancellable { get; set; } + + /// + /// Optional progress percentage to display (value range: 0 to 100). If omitted no percentage will be shown. + /// + [Optional] + public int? Percentage { get; set; } + } +} diff --git a/src/Dap.Protocol/Events/ProgressUpdateEvent.cs b/src/Dap.Protocol/Events/ProgressUpdateEvent.cs new file mode 100644 index 000000000..8030b9e0e --- /dev/null +++ b/src/Dap.Protocol/Events/ProgressUpdateEvent.cs @@ -0,0 +1,17 @@ +using MediatR; +using OmniSharp.Extensions.DebugAdapter.Protocol.Models; +using OmniSharp.Extensions.DebugAdapter.Protocol.Serialization; +using OmniSharp.Extensions.JsonRpc; + +namespace OmniSharp.Extensions.DebugAdapter.Protocol.Events +{ + [Method(EventNames.ProgressUpdate, Direction.ServerToClient)] + public class ProgressUpdateEvent : ProgressEvent, IRequest + { + /// + /// Optional progress percentage to display (value range: 0 to 100). If omitted no percentage will be shown. + /// + [Optional] + public double? Percentage { get; set; } + } +} diff --git a/src/Dap.Protocol/Events/TerminatedEvent.cs b/src/Dap.Protocol/Events/TerminatedEvent.cs index 7e3350d48..275c0ca47 100644 --- a/src/Dap.Protocol/Events/TerminatedEvent.cs +++ b/src/Dap.Protocol/Events/TerminatedEvent.cs @@ -1,6 +1,7 @@ using Newtonsoft.Json.Linq; using OmniSharp.Extensions.DebugAdapter.Protocol.Serialization; using MediatR; +using Newtonsoft.Json; using OmniSharp.Extensions.JsonRpc; namespace OmniSharp.Extensions.DebugAdapter.Protocol.Events @@ -8,12 +9,13 @@ namespace OmniSharp.Extensions.DebugAdapter.Protocol.Events [Method(EventNames.Terminated, Direction.ServerToClient)] public class TerminatedEvent : IRequest { - /// /// A debug adapter may set 'restart' to true (or to an arbitrary object) to request that the front end restarts the session. /// The value is not interpreted by the client and passed unmodified as an attribute '__restart' to the 'launch' and 'attach' requests. /// - [Optional] public JToken Restart { get; set; } + [Optional] + [JsonProperty(PropertyName = "__restart")] + public JToken Restart { get; set; } } } diff --git a/src/Dap.Protocol/IClientProgressManager.cs b/src/Dap.Protocol/IClientProgressManager.cs new file mode 100644 index 000000000..ea4bc1d82 --- /dev/null +++ b/src/Dap.Protocol/IClientProgressManager.cs @@ -0,0 +1,9 @@ +using System; + +namespace OmniSharp.Extensions.DebugAdapter.Client +{ + public interface IClientProgressManager + { + IObservable Progress { get; } + } +} \ No newline at end of file diff --git a/src/Dap.Protocol/IDebugAdapterClient.cs b/src/Dap.Protocol/IDebugAdapterClient.cs index 5bdbf3d82..04e90f0c6 100644 --- a/src/Dap.Protocol/IDebugAdapterClient.cs +++ b/src/Dap.Protocol/IDebugAdapterClient.cs @@ -1,8 +1,17 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using OmniSharp.Extensions.DebugAdapter.Client; +using OmniSharp.Extensions.DebugAdapter.Protocol.Requests; using OmniSharp.Extensions.JsonRpc; namespace OmniSharp.Extensions.DebugAdapter.Protocol { - public interface IDebugAdapterClient : IResponseRouter + public interface IDebugAdapterClient : IResponseRouter, IDisposable { + Task Initialize(CancellationToken token); + IClientProgressManager ProgressManager { get; } + InitializeRequestArguments ClientSettings { get; } + InitializeResponse ServerSettings { get; } } } diff --git a/src/Dap.Protocol/IDebugAdapterServer.cs b/src/Dap.Protocol/IDebugAdapterServer.cs index 5d914ba77..2e1e59221 100644 --- a/src/Dap.Protocol/IDebugAdapterServer.cs +++ b/src/Dap.Protocol/IDebugAdapterServer.cs @@ -1,8 +1,17 @@ -using OmniSharp.Extensions.JsonRpc; +using System; +using System.Threading; +using System.Threading.Tasks; +using OmniSharp.Extensions.DebugAdapter.Protocol.Requests; +using OmniSharp.Extensions.DebugAdapter.Server; +using OmniSharp.Extensions.JsonRpc; namespace OmniSharp.Extensions.DebugAdapter.Protocol { - public interface IDebugAdapterServer : IResponseRouter + public interface IDebugAdapterServer : IResponseRouter, IDisposable { + Task Initialize(CancellationToken token); + IServerProgressManager ProgressManager { get; } + InitializeRequestArguments ClientSettings { get; } + InitializeResponse ServerSettings { get; } } } diff --git a/src/Dap.Protocol/IProgressObservable.cs b/src/Dap.Protocol/IProgressObservable.cs new file mode 100644 index 000000000..8fc6ddef4 --- /dev/null +++ b/src/Dap.Protocol/IProgressObservable.cs @@ -0,0 +1,9 @@ +using System; +using OmniSharp.Extensions.DebugAdapter.Protocol.Events; + +namespace OmniSharp.Extensions.DebugAdapter.Client +{ + public interface IProgressObservable : IObservable + { + } +} \ No newline at end of file diff --git a/src/Dap.Protocol/IProgressObserver.cs b/src/Dap.Protocol/IProgressObserver.cs new file mode 100644 index 000000000..999b6ff9f --- /dev/null +++ b/src/Dap.Protocol/IProgressObserver.cs @@ -0,0 +1,12 @@ +using System; +using OmniSharp.Extensions.DebugAdapter.Protocol.Events; +using OmniSharp.Extensions.DebugAdapter.Protocol.Models; + +namespace OmniSharp.Extensions.DebugAdapter.Server +{ + public interface IProgressObserver : IObserver, IDisposable + { + ProgressToken ProgressId { get; } + void OnNext(string message, double? percentage); + } +} \ No newline at end of file diff --git a/src/Dap.Protocol/IServerProgressManager.cs b/src/Dap.Protocol/IServerProgressManager.cs new file mode 100644 index 000000000..027671a43 --- /dev/null +++ b/src/Dap.Protocol/IServerProgressManager.cs @@ -0,0 +1,15 @@ +using System; +using System.Threading; +using OmniSharp.Extensions.DebugAdapter.Protocol.Events; + +namespace OmniSharp.Extensions.DebugAdapter.Server +{ + public interface IServerProgressManager + { + /// + /// Creates a that will send all of its progress information to the same source. + /// The other side can cancel this, so the should be respected. + /// + IProgressObserver Create(ProgressStartEvent begin, Func onError = null, Func onComplete = null); + } +} \ No newline at end of file diff --git a/src/Dap.Protocol/Models/Breakpoint.cs b/src/Dap.Protocol/Models/Breakpoint.cs index eb70fe9bf..fea696fe2 100644 --- a/src/Dap.Protocol/Models/Breakpoint.cs +++ b/src/Dap.Protocol/Models/Breakpoint.cs @@ -5,7 +5,6 @@ namespace OmniSharp.Extensions.DebugAdapter.Protocol.Models /// /// Information about a Breakpoint created in setBreakpoints or setFunctionBreakpoints. /// - public class Breakpoint { /// @@ -47,5 +46,16 @@ public class Breakpoint /// An optional end column of the actual range covered by the breakpoint. If no end line is given, then the end column is assumed to be in the start line. /// [Optional] public int? EndColumn { get; set; } + + /// + /// An optional memory reference to where the breakpoint is set. + /// + [Optional] public string InstructionReference { get; set; } + + /// + /// An optional offset from the instruction reference. + /// This can be negative. + /// + [Optional] public int? Offset { get; set; } } } diff --git a/src/Dap.Protocol/Models/BreakpointLocation.cs b/src/Dap.Protocol/Models/BreakpointLocation.cs new file mode 100644 index 000000000..7ef7632b8 --- /dev/null +++ b/src/Dap.Protocol/Models/BreakpointLocation.cs @@ -0,0 +1,30 @@ +using OmniSharp.Extensions.DebugAdapter.Protocol.Serialization; + +namespace OmniSharp.Extensions.DebugAdapter.Protocol.Models +{ + public class BreakpointLocation + { + /// + /// Start line of breakpoint location. + /// + public int Line { get; set; } + + /// + /// Optional start column of breakpoint location. + /// + [Optional] + public int Column { get; set; } + + /// + /// Optional end line of breakpoint location if the location covers a range. + /// + [Optional] + public int EndLine { get; set; } + + /// + /// Optional end column of breakpoint location if the location covers a range. + /// + [Optional] + public int EndColumn { get; set; } + } +} diff --git a/src/Dap.Protocol/Models/Capabilities.cs b/src/Dap.Protocol/Models/Capabilities.cs index 47ab81523..736ab0325 100644 --- a/src/Dap.Protocol/Models/Capabilities.cs +++ b/src/Dap.Protocol/Models/Capabilities.cs @@ -151,5 +151,30 @@ public class Capabilities /// The debug adapter supports the 'disassemble' request. /// [Optional] public bool? SupportsDisassembleRequest { get; set; } + + /// + /// The debug adapter supports the 'cancel' request. + /// + [Optional] public bool? SupportsCancelRequest { get; set; } + + /// + /// The debug adapter supports the 'breakpointLocations' request. + /// + [Optional] public bool? SupportsBreakpointLocationsRequest { get; set; } + + /// + /// The debug adapter supports the 'clipboard' context value in the 'evaluate' request. + /// + [Optional] public bool? SupportsClipboardContext { get; set; } + + /// + /// The debug adapter supports stepping granularities (argument 'granularity') for the stepping requests. + /// + [Optional] public bool? SupportsSteppingGranularity { get; set; } + + /// + /// The debug adapter supports adding breakpoints based on instruction references. + /// + [Optional] public bool? SupportsInstructionBreakpoints { get; set; } } } diff --git a/src/Dap.Protocol/Models/ColumnDescriptor.cs b/src/Dap.Protocol/Models/ColumnDescriptor.cs index 8049410a1..ec7894055 100644 --- a/src/Dap.Protocol/Models/ColumnDescriptor.cs +++ b/src/Dap.Protocol/Models/ColumnDescriptor.cs @@ -27,11 +27,11 @@ public class ColumnDescriptor /// /// Datatype of values in this column. Defaults to 'string' if not specified. /// - [Optional] public ColumnDescriptorType Type { get; set; } + [Optional] public ColumnDescriptorType? Type { get; set; } /// /// Width of this column in characters (hint only). /// [Optional] public long? Width { get; set; } } -} \ No newline at end of file +} diff --git a/src/Dap.Protocol/Models/CompletionItem.cs b/src/Dap.Protocol/Models/CompletionItem.cs index ac02dc6f9..d9555e793 100644 --- a/src/Dap.Protocol/Models/CompletionItem.cs +++ b/src/Dap.Protocol/Models/CompletionItem.cs @@ -33,5 +33,19 @@ public class CompletionItem /// If missing the value 0 is assumed which results in the completion text being inserted. /// [Optional] public int? Length { get; set; } + + /// + /// Determines the start of the new selection after the text has been inserted (or replaced). + /// The start position must in the range 0 and length of the completion text. + /// If omitted the selection starts at the end of the completion text. + /// + [Optional] public int? SelectionStart { get; set; } + + /// + /// Determines the length of the new selection after the text has been inserted (or replaced). + /// The selection can not extend beyond the bounds of the completion text. + /// If omitted the length is assumed to be 0. + /// + [Optional] public int? SelectionLength { get; set; } } } diff --git a/src/Dap.Protocol/Models/DataBreakpoint.cs b/src/Dap.Protocol/Models/DataBreakpoint.cs index 92a0bbf1e..e49e2ac14 100644 --- a/src/Dap.Protocol/Models/DataBreakpoint.cs +++ b/src/Dap.Protocol/Models/DataBreakpoint.cs @@ -15,7 +15,7 @@ public class DataBreakpoint /// /// The access type of the data. /// - [Optional] public DataBreakpointAccessType AccessType { get; set; } + [Optional] public DataBreakpointAccessType? AccessType { get; set; } /// /// An optional expression for conditional breakpoints. @@ -27,4 +27,4 @@ public class DataBreakpoint /// [Optional] public string HitCondition { get; set; } } -} \ No newline at end of file +} diff --git a/src/Dap.Protocol/Models/InstructionBreakpoint.cs b/src/Dap.Protocol/Models/InstructionBreakpoint.cs new file mode 100644 index 000000000..15a8a395c --- /dev/null +++ b/src/Dap.Protocol/Models/InstructionBreakpoint.cs @@ -0,0 +1,33 @@ +using OmniSharp.Extensions.DebugAdapter.Protocol.Serialization; + +namespace OmniSharp.Extensions.DebugAdapter.Protocol.Models +{ + public class InstructionBreakpoint + { + /// + /// The instruction reference of the breakpoint. + /// This should be a memory or instruction pointer reference from an EvaluateResponse, Variable, StackFrame, GotoTarget, or Breakpoint. + /// + public string InstructionReference { get; set; } + + /// + /// An optional offset from the instruction reference. + /// This can be negative. + /// + [Optional] public int Offset { get; set; } + + /// + /// An optional expression for conditional breakpoints. + /// It is only honored by a debug adapter if the capability 'supportsConditionalBreakpoints' is true. + /// + [Optional] public string Condition { get; set; } + + /// + /// An optional expression that controls how many hits of the breakpoint are ignored. + /// The backend is expected to interpret the expression as needed. + /// The attribute is only honored by a debug adapter if the capability 'supportsHitConditionalBreakpoints' is true. + /// + [Optional] public string HitCondition { get; set; } + + } +} diff --git a/src/Dap.Protocol/Models/Module.cs b/src/Dap.Protocol/Models/Module.cs index cc038c086..3e4e5967c 100644 --- a/src/Dap.Protocol/Models/Module.cs +++ b/src/Dap.Protocol/Models/Module.cs @@ -1,4 +1,6 @@ -using OmniSharp.Extensions.DebugAdapter.Protocol.Serialization; +using System.Collections.Generic; +using Newtonsoft.Json; +using OmniSharp.Extensions.DebugAdapter.Protocol.Serialization; namespace OmniSharp.Extensions.DebugAdapter.Protocol.Models { @@ -65,5 +67,10 @@ public class Module /// Address range covered by this module. /// [Optional] public string AddressRange { get; set; } + + /// + /// Allows additional data to be displayed + /// + [JsonExtensionData] public Dictionary ExtensionData { get; set; } = new Dictionary(); } -} \ No newline at end of file +} diff --git a/src/Dap.Protocol/Models/ProgressToken.cs b/src/Dap.Protocol/Models/ProgressToken.cs new file mode 100644 index 000000000..163fa4332 --- /dev/null +++ b/src/Dap.Protocol/Models/ProgressToken.cs @@ -0,0 +1,93 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using Newtonsoft.Json; + +namespace OmniSharp.Extensions.DebugAdapter.Protocol.Models +{ + [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] + public struct ProgressToken : IEquatable, IEquatable, IEquatable + { + private long? _long; + private string _string; + + public ProgressToken(long value) + { + _long = value; + _string = null; + } + public ProgressToken(string value) + { + _long = null; + _string = value; + } + + public bool IsLong => _long.HasValue; + public long Long + { + get => _long ?? 0; + set { + String = null; + _long = value; + } + } + + public bool IsString => _string != null; + public string String + { + get => _string; + set { + _string = value; + _long = null; + } + } + + public static implicit operator ProgressToken(long value) + { + return new ProgressToken(value); + } + + public static implicit operator ProgressToken(string value) + { + return new ProgressToken(value); + } + + public override bool Equals(object obj) + { + return obj is ProgressToken token && + this.Equals(token); + } + + public override int GetHashCode() + { + var hashCode = 1456509845; + hashCode = hashCode * -1521134295 + IsLong.GetHashCode(); + hashCode = hashCode * -1521134295 + Long.GetHashCode(); + hashCode = hashCode * -1521134295 + IsString.GetHashCode(); + hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(String); + return hashCode; + } + + public bool Equals(ProgressToken other) + { + return IsLong == other.IsLong && + Long == other.Long && + IsString == other.IsString && + String == other.String; + } + + public bool Equals(long other) + { + return this.IsLong && this.Long == other; + } + + public bool Equals(string other) + { + return this.IsString && this.String == other; + } + + private string DebuggerDisplay => IsString ? String : IsLong ? Long.ToString() : ""; + /// + public override string ToString() => DebuggerDisplay; + } +} diff --git a/src/Dap.Protocol/Models/Source.cs b/src/Dap.Protocol/Models/Source.cs index 47fdf2b01..853f0bbc0 100644 --- a/src/Dap.Protocol/Models/Source.cs +++ b/src/Dap.Protocol/Models/Source.cs @@ -26,7 +26,7 @@ public class Source /// /// An optional hint for how to present the source in the UI. A value of 'deemphasize' can be used to indicate that the source is not available or that it is skipped on stepping. /// - [Optional] public SourcePresentationHint PresentationHint { get; set; } + [Optional] public SourcePresentationHint? PresentationHint { get; set; } /// /// The (optional) origin of this source: possible values 'internal module', 'inlined content from source map', etc. diff --git a/src/Dap.Protocol/Models/StackFrame.cs b/src/Dap.Protocol/Models/StackFrame.cs index 9039e996a..43a357f2a 100644 --- a/src/Dap.Protocol/Models/StackFrame.cs +++ b/src/Dap.Protocol/Models/StackFrame.cs @@ -55,6 +55,6 @@ public class StackFrame /// /// An optional hint for how to present this frame in the UI. A value of 'label' can be used to indicate that the frame is an artificial frame that is used as a visual label or separator. A value of 'subtle' can be used to change the appearance of a frame in a 'subtle' way. /// - [Optional] public StackFramePresentationHint PresentationHint { get; set; } + [Optional] public StackFramePresentationHint? PresentationHint { get; set; } } } diff --git a/src/Dap.Protocol/Models/SteppingGranularity.cs b/src/Dap.Protocol/Models/SteppingGranularity.cs new file mode 100644 index 000000000..5f7a465e4 --- /dev/null +++ b/src/Dap.Protocol/Models/SteppingGranularity.cs @@ -0,0 +1,13 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; + +namespace OmniSharp.Extensions.DebugAdapter.Protocol.Models +{ + [JsonConverter(typeof(StringEnumConverter))] + public enum SteppingGranularity + { + Statement, + Line, + Instruction, + } +} diff --git a/src/Dap.Protocol/OnClientStartedDelegate.cs b/src/Dap.Protocol/OnClientStartedDelegate.cs new file mode 100644 index 000000000..b029ab783 --- /dev/null +++ b/src/Dap.Protocol/OnClientStartedDelegate.cs @@ -0,0 +1,8 @@ +using System.Threading; +using System.Threading.Tasks; +using OmniSharp.Extensions.DebugAdapter.Protocol.Requests; + +namespace OmniSharp.Extensions.DebugAdapter.Protocol +{ + public delegate Task OnClientStartedDelegate(IDebugAdapterClient client, InitializeResponse result, CancellationToken cancellationToken); +} \ No newline at end of file diff --git a/src/Dap.Protocol/OnServerStartedDelegate.cs b/src/Dap.Protocol/OnServerStartedDelegate.cs new file mode 100644 index 000000000..fe97e5f42 --- /dev/null +++ b/src/Dap.Protocol/OnServerStartedDelegate.cs @@ -0,0 +1,8 @@ +using System.Threading; +using System.Threading.Tasks; +using OmniSharp.Extensions.DebugAdapter.Protocol.Requests; + +namespace OmniSharp.Extensions.DebugAdapter.Protocol +{ + public delegate Task OnServerStartedDelegate(IDebugAdapterServer server, InitializeResponse result, CancellationToken cancellationToken); +} \ No newline at end of file diff --git a/src/Dap.Protocol/Requests/AttachRequestArguments.cs b/src/Dap.Protocol/Requests/AttachRequestArguments.cs index 8dc763328..9cba9163f 100644 --- a/src/Dap.Protocol/Requests/AttachRequestArguments.cs +++ b/src/Dap.Protocol/Requests/AttachRequestArguments.cs @@ -1,6 +1,9 @@ +using System.Collections; +using System.Collections.Generic; using Newtonsoft.Json.Linq; using OmniSharp.Extensions.DebugAdapter.Protocol.Serialization; using MediatR; +using Newtonsoft.Json; using OmniSharp.Extensions.JsonRpc; namespace OmniSharp.Extensions.DebugAdapter.Protocol.Requests @@ -14,7 +17,10 @@ public class AttachRequestArguments : IRequest /// The client should leave the data intact. /// [Optional] + [JsonProperty(PropertyName = "__restart")] public JToken Restart { get; set; } - } + [JsonExtensionData] + public IDictionary ExtensionData { get; set; } = new Dictionary(); + } } diff --git a/src/Dap.Protocol/Requests/BreakpointLocationsArguments.cs b/src/Dap.Protocol/Requests/BreakpointLocationsArguments.cs new file mode 100644 index 000000000..f675c6351 --- /dev/null +++ b/src/Dap.Protocol/Requests/BreakpointLocationsArguments.cs @@ -0,0 +1,38 @@ +using MediatR; +using OmniSharp.Extensions.DebugAdapter.Protocol.Models; +using OmniSharp.Extensions.DebugAdapter.Protocol.Serialization; +using OmniSharp.Extensions.JsonRpc; + +namespace OmniSharp.Extensions.DebugAdapter.Protocol.Requests +{ + [Method(RequestNames.BreakpointLocations, Direction.ClientToServer)] + public class BreakpointLocationsArguments : IRequest { + /// + /// The source location of the breakpoints; either 'source.path' or 'source.reference' must be specified. + /// + public Source Source { get; set; } + + /// + /// Start line of range to search possible breakpoint locations in. If only the line is specified, the request returns all possible locations in that line. + /// + public int Line { get; set; } + + /// + /// Optional start column of range to search possible breakpoint locations in. If no start column is given, the first column in the start line is assumed. + /// + [Optional] + public int? Column { get; set; } + + /// + /// Optional end line of range to search possible breakpoint locations in. If no end line is given, then the end line is assumed to be the start line. + /// + [Optional] + public int? EndLine { get; set; } + + /// + /// Optional end column of range to search possible breakpoint locations in. If no end column is given, then it is assumed to be in the last column of the end line. + /// + [Optional] + public int? EndColumn { get; set; } + } +} \ No newline at end of file diff --git a/src/Dap.Protocol/Requests/BreakpointLocationsResponse.cs b/src/Dap.Protocol/Requests/BreakpointLocationsResponse.cs new file mode 100644 index 000000000..62ca3714d --- /dev/null +++ b/src/Dap.Protocol/Requests/BreakpointLocationsResponse.cs @@ -0,0 +1,11 @@ +using OmniSharp.Extensions.DebugAdapter.Protocol.Models; + +namespace OmniSharp.Extensions.DebugAdapter.Protocol.Requests +{ + public class BreakpointLocationsResponse { + /// + /// Sorted set of possible breakpoint locations. + /// + public Container Breakpoints { get; set; } + } +} \ No newline at end of file diff --git a/src/Dap.Protocol/Requests/CancelArguments.cs b/src/Dap.Protocol/Requests/CancelArguments.cs new file mode 100644 index 000000000..ba7bf2fe9 --- /dev/null +++ b/src/Dap.Protocol/Requests/CancelArguments.cs @@ -0,0 +1,30 @@ +using MediatR; +using OmniSharp.Extensions.DebugAdapter.Protocol.Models; +using OmniSharp.Extensions.DebugAdapter.Protocol.Serialization; +using OmniSharp.Extensions.JsonRpc; + +namespace OmniSharp.Extensions.DebugAdapter.Protocol.Requests +{ + /// + /// DAP is kind of silly.... + /// Cancellation is for requests and progress tokens... hopefully if isn't ever expanded any further... because that would be fun. + /// + [Method(RequestNames.Cancel, Direction.ClientToServer)] + public class CancelArguments : IRequest + { + // This is removed on purpose, as request cancellation is handled by the DapReciever + // /// + // /// The ID (attribute 'seq') of the request to cancel. If missing no request is cancelled. + // /// Both a 'requestId' and a 'progressId' can be specified in one request. + // /// + // [Optional] + // public int? RequestId { get; set; } + + /// + /// The ID (attribute 'progressId') of the progress to cancel. If missing no progress is cancelled. + /// Both a 'requestId' and a 'progressId' can be specified in one request. + /// + [Optional] + public ProgressToken? ProgressId { get; set; } + } +} diff --git a/src/Dap.Protocol/Requests/CancelResponse.cs b/src/Dap.Protocol/Requests/CancelResponse.cs new file mode 100644 index 000000000..e6e2c40ae --- /dev/null +++ b/src/Dap.Protocol/Requests/CancelResponse.cs @@ -0,0 +1,22 @@ +using OmniSharp.Extensions.DebugAdapter.Protocol.Models; +using OmniSharp.Extensions.DebugAdapter.Protocol.Serialization; + +namespace OmniSharp.Extensions.DebugAdapter.Protocol.Requests +{ + public class CancelResponse + { + /// + /// The ID (attribute 'seq') of the request to cancel. If missing no request is cancelled. + /// Both a 'requestId' and a 'progressId' can be specified in one request. + /// + [Optional] + public int? RequestId { get; set; } + + /// + /// The ID (attribute 'progressId') of the progress to cancel. If missing no progress is cancelled. + /// Both a 'requestId' and a 'progressId' can be specified in one request. + /// + [Optional] + public ProgressToken? ProgressId { get; set; } + } +} diff --git a/src/Dap.Protocol/Requests/ContinueArguments.cs b/src/Dap.Protocol/Requests/ContinueArguments.cs index 6c00aa865..e77b8407d 100644 --- a/src/Dap.Protocol/Requests/ContinueArguments.cs +++ b/src/Dap.Protocol/Requests/ContinueArguments.cs @@ -1,3 +1,5 @@ +using System.Threading; +using System.Threading.Tasks; using MediatR; using OmniSharp.Extensions.JsonRpc; @@ -11,5 +13,4 @@ public class ContinueArguments : IRequest /// public long ThreadId { get; set; } } - } diff --git a/src/Dap.Protocol/Requests/IAttachHandler.cs b/src/Dap.Protocol/Requests/IAttachHandler.cs index 04b074f7d..6b1df0afe 100644 --- a/src/Dap.Protocol/Requests/IAttachHandler.cs +++ b/src/Dap.Protocol/Requests/IAttachHandler.cs @@ -6,11 +6,21 @@ namespace OmniSharp.Extensions.DebugAdapter.Protocol.Requests { [Parallel, Method(RequestNames.Attach, Direction.ClientToServer)] - [GenerateHandlerMethods, GenerateRequestMethods] - public interface IAttachHandler : IJsonRpcRequestHandler { } + [GenerateHandlerMethods(AllowDerivedRequests = true), GenerateRequestMethods] + public interface IAttachHandler : IJsonRpcRequestHandler where T : AttachRequestArguments + { + } + + public interface IAttachHandler : IAttachHandler + { + } + + public abstract class AttachHandlerBase : IAttachHandler where T : AttachRequestArguments + { + public abstract Task Handle(T request, CancellationToken cancellationToken); + } - public abstract class AttachHandler : IAttachHandler + public abstract class AttachHandler : AttachHandlerBase { - public abstract Task Handle(AttachRequestArguments request, CancellationToken cancellationToken); } } diff --git a/src/Dap.Protocol/Requests/IBreakpointLocationsHandler.cs b/src/Dap.Protocol/Requests/IBreakpointLocationsHandler.cs new file mode 100644 index 000000000..a175541de --- /dev/null +++ b/src/Dap.Protocol/Requests/IBreakpointLocationsHandler.cs @@ -0,0 +1,18 @@ +using System.Threading; +using System.Threading.Tasks; +using OmniSharp.Extensions.JsonRpc; +using OmniSharp.Extensions.JsonRpc.Generation; + +namespace OmniSharp.Extensions.DebugAdapter.Protocol.Requests +{ + [Parallel, Method(RequestNames.BreakpointLocations, Direction.ClientToServer)] + [GenerateHandlerMethods, GenerateRequestMethods] + public interface IBreakpointLocationsHandler : IJsonRpcRequestHandler + { + } + + public abstract class BreakpointLocationsHandlerBase : IBreakpointLocationsHandler + { + public abstract Task Handle(BreakpointLocationsArguments request, CancellationToken cancellationToken); + } +} diff --git a/src/Dap.Protocol/Requests/ICancelHandler.cs b/src/Dap.Protocol/Requests/ICancelHandler.cs new file mode 100644 index 000000000..2de9af4f6 --- /dev/null +++ b/src/Dap.Protocol/Requests/ICancelHandler.cs @@ -0,0 +1,22 @@ +using System.Threading; +using System.Threading.Tasks; +using OmniSharp.Extensions.JsonRpc; +using OmniSharp.Extensions.JsonRpc.Generation; + +namespace OmniSharp.Extensions.DebugAdapter.Protocol.Requests +{ + /// + /// DAP is kind of silly.... + /// Cancellation is for requests and progress tokens... hopefully if isn't ever expanded any further... because that would be fun. + /// + [Parallel, Method(RequestNames.Cancel, Direction.ClientToServer)] + [GenerateHandlerMethods, GenerateRequestMethods] + public interface ICancelHandler : IJsonRpcRequestHandler + { + } + + public abstract class CancelHandlerBase : ICancelHandler + { + public abstract Task Handle(CancelArguments request, CancellationToken cancellationToken); + } +} diff --git a/src/Dap.Protocol/Requests/IDataBreakpointInfoHandler.cs b/src/Dap.Protocol/Requests/IDataBreakpointInfoHandler.cs index eb3ea46f8..021d603a3 100644 --- a/src/Dap.Protocol/Requests/IDataBreakpointInfoHandler.cs +++ b/src/Dap.Protocol/Requests/IDataBreakpointInfoHandler.cs @@ -2,10 +2,12 @@ using System.Threading; using System.Threading.Tasks; using OmniSharp.Extensions.JsonRpc; +using OmniSharp.Extensions.JsonRpc.Generation; namespace OmniSharp.Extensions.DebugAdapter.Protocol.Requests { [Parallel, Method(RequestNames.DataBreakpointInfo, Direction.ClientToServer)] + [GenerateHandlerMethods, GenerateRequestMethods] public interface IDataBreakpointInfoHandler : IJsonRpcRequestHandler { @@ -16,24 +18,4 @@ public abstract class DataBreakpointInfoHandler : IDataBreakpointInfoHandler public abstract Task Handle(DataBreakpointInfoArguments request, CancellationToken cancellationToken); } - - public static class DataBreakpointInfoExtensions - { - public static IDebugAdapterServerRegistry OnDataBreakpointInfo(this IDebugAdapterServerRegistry registry, - Func> handler) - { - return registry.AddHandler(RequestNames.DataBreakpointInfo, RequestHandler.For(handler)); - } - - public static IDebugAdapterServerRegistry OnDataBreakpointInfo(this IDebugAdapterServerRegistry registry, - Func> handler) - { - return registry.AddHandler(RequestNames.DataBreakpointInfo, RequestHandler.For(handler)); - } - - public static Task RequestDataBreakpointInfo(this IDebugAdapterClient mediator, DataBreakpointInfoArguments @params, CancellationToken cancellationToken = default) - { - return mediator.SendRequest(@params, cancellationToken); - } - } } diff --git a/src/Dap.Protocol/Requests/IInitializeRequestArguments.cs b/src/Dap.Protocol/Requests/IInitializeRequestArguments.cs new file mode 100644 index 000000000..7d768d599 --- /dev/null +++ b/src/Dap.Protocol/Requests/IInitializeRequestArguments.cs @@ -0,0 +1,66 @@ +namespace OmniSharp.Extensions.DebugAdapter.Protocol.Requests +{ + public interface IInitializeRequestArguments + { + /// + /// The ID of the(frontend) client using this adapter. + /// + string ClientId { get; set; } + + /// + /// The human readable name of the(frontend) client using this adapter. + /// + string ClientName { get; set; } + + /// + /// The ID of the debug adapter. + /// + string AdapterId { get; set; } + + /// + /// The ISO-639 locale of the(frontend) client using this adapter, e.g.en-US or de-CH. + /// + string Locale { get; set; } + + /// + /// If true all line numbers are 1-based(default). + /// + bool? LinesStartAt1 { get; set; } + + /// + /// If true all column numbers are 1-based(default). + /// + bool? ColumnsStartAt1 { get; set; } + + /// + /// Determines in what format paths are specified.The default is 'path', which is the native format. + /// Values: 'path', 'uri', etc. + /// + string PathFormat { get; set; } + + /// + /// Client supports the optional type attribute for variables. + /// + bool? SupportsVariableType { get; set; } + + /// + /// Client supports the paging of variables. + /// + bool? SupportsVariablePaging { get; set; } + + /// + /// Client supports the runInTerminal request. + /// + bool? SupportsRunInTerminalRequest { get; set; } + + /// + /// Client supports memory references. + /// + bool? SupportsMemoryReferences { get; set; } + + /// + /// Client supports progress reporting. + /// + bool? SupportsProgressReporting { get; set; } + } +} diff --git a/src/Dap.Protocol/Requests/ILaunchHandler.cs b/src/Dap.Protocol/Requests/ILaunchHandler.cs index 21f95f103..44b8a6e92 100644 --- a/src/Dap.Protocol/Requests/ILaunchHandler.cs +++ b/src/Dap.Protocol/Requests/ILaunchHandler.cs @@ -6,14 +6,21 @@ namespace OmniSharp.Extensions.DebugAdapter.Protocol.Requests { [Parallel, Method(RequestNames.Launch, Direction.ClientToServer)] - [GenerateHandlerMethods, GenerateRequestMethods] - public interface ILaunchHandler : IJsonRpcRequestHandler + [GenerateHandlerMethods(AllowDerivedRequests = true), GenerateRequestMethods] + public interface ILaunchHandler : IJsonRpcRequestHandler where T : LaunchRequestArguments { } - public abstract class LaunchHandler : ILaunchHandler + public interface ILaunchHandler : ILaunchHandler + { + } + + public abstract class LaunchHandlerBase : ILaunchHandler where T : LaunchRequestArguments + { + public abstract Task Handle(T request, CancellationToken cancellationToken); + } + + public abstract class LaunchHandler : LaunchHandlerBase { - public abstract Task - Handle(LaunchRequestArguments request, CancellationToken cancellationToken); } } diff --git a/src/Dap.Protocol/Requests/IRestartFrameHandler.cs b/src/Dap.Protocol/Requests/IRestartFrameHandler.cs index d946a64f3..c3eff9da0 100644 --- a/src/Dap.Protocol/Requests/IRestartFrameHandler.cs +++ b/src/Dap.Protocol/Requests/IRestartFrameHandler.cs @@ -2,10 +2,12 @@ using System.Threading; using System.Threading.Tasks; using OmniSharp.Extensions.JsonRpc; +using OmniSharp.Extensions.JsonRpc.Generation; namespace OmniSharp.Extensions.DebugAdapter.Protocol.Requests { [Parallel, Method(RequestNames.RestartFrame, Direction.ClientToServer)] + [GenerateHandlerMethods, GenerateRequestMethods] public interface IRestartFrameHandler : IJsonRpcRequestHandler { } @@ -15,24 +17,4 @@ public abstract class RestartFrameHandler : IRestartFrameHandler public abstract Task Handle(RestartFrameArguments request, CancellationToken cancellationToken); } - - public static class RestartFrameExtensions - { - public static IDebugAdapterServerRegistry OnRestartFrame(this IDebugAdapterServerRegistry registry, - Func> handler) - { - return registry.AddHandler(RequestNames.RestartFrame, RequestHandler.For(handler)); - } - - public static IDebugAdapterServerRegistry OnRestartFrame(this IDebugAdapterServerRegistry registry, - Func> handler) - { - return registry.AddHandler(RequestNames.RestartFrame, RequestHandler.For(handler)); - } - - public static Task RequestRestartFrame(this IDebugAdapterClient mediator, RestartFrameArguments @params, CancellationToken cancellationToken = default) - { - return mediator.SendRequest(@params, cancellationToken); - } - } } diff --git a/src/Dap.Protocol/Requests/ISetInstructionBreakpointsHandler.cs b/src/Dap.Protocol/Requests/ISetInstructionBreakpointsHandler.cs new file mode 100644 index 000000000..9bdc2f10e --- /dev/null +++ b/src/Dap.Protocol/Requests/ISetInstructionBreakpointsHandler.cs @@ -0,0 +1,18 @@ +using System.Threading; +using System.Threading.Tasks; +using OmniSharp.Extensions.JsonRpc; +using OmniSharp.Extensions.JsonRpc.Generation; + +namespace OmniSharp.Extensions.DebugAdapter.Protocol.Requests +{ + [Parallel, Method(RequestNames.SetInstructionBreakpoints, Direction.ClientToServer)] + [GenerateHandlerMethods, GenerateRequestMethods] + public interface ISetInstructionBreakpointsHandler : IJsonRpcRequestHandler + { + } + + public abstract class SetInstructionBreakpointsHandlerBase : ISetInstructionBreakpointsHandler + { + public abstract Task Handle(SetInstructionBreakpointsArguments request, CancellationToken cancellationToken); + } +} diff --git a/src/Dap.Protocol/Requests/IVariablesHandler.cs b/src/Dap.Protocol/Requests/IVariablesHandler.cs index 64885e1ee..76d57ea36 100644 --- a/src/Dap.Protocol/Requests/IVariablesHandler.cs +++ b/src/Dap.Protocol/Requests/IVariablesHandler.cs @@ -2,10 +2,12 @@ using System.Threading; using System.Threading.Tasks; using OmniSharp.Extensions.JsonRpc; +using OmniSharp.Extensions.JsonRpc.Generation; namespace OmniSharp.Extensions.DebugAdapter.Protocol.Requests { [Parallel, Method(RequestNames.Variables, Direction.ClientToServer)] + [GenerateHandlerMethods, GenerateRequestMethods] public interface IVariablesHandler : IJsonRpcRequestHandler { } @@ -14,24 +16,4 @@ public abstract class VariablesHandler : IVariablesHandler { public abstract Task Handle(VariablesArguments request, CancellationToken cancellationToken); } - - public static class VariablesExtensions - { - public static IDebugAdapterServerRegistry OnVariables(this IDebugAdapterServerRegistry registry, - Func> handler) - { - return registry.AddHandler(RequestNames.Variables, RequestHandler.For(handler)); - } - - public static IDebugAdapterServerRegistry OnVariables(this IDebugAdapterServerRegistry registry, - Func> handler) - { - return registry.AddHandler(RequestNames.Variables, RequestHandler.For(handler)); - } - - public static Task RequestVariables(this IDebugAdapterClient mediator, VariablesArguments @params, CancellationToken cancellationToken = default) - { - return mediator.SendRequest(@params, cancellationToken); - } - } } diff --git a/src/Dap.Protocol/Requests/InitializeRequestArguments.cs b/src/Dap.Protocol/Requests/InitializeRequestArguments.cs index e7d2c5250..50a82160d 100644 --- a/src/Dap.Protocol/Requests/InitializeRequestArguments.cs +++ b/src/Dap.Protocol/Requests/InitializeRequestArguments.cs @@ -5,7 +5,7 @@ namespace OmniSharp.Extensions.DebugAdapter.Protocol.Requests { [Method(RequestNames.Initialize, Direction.ClientToServer)] - public class InitializeRequestArguments : IRequest + public class InitializeRequestArguments : IRequest, IInitializeRequestArguments { /// /// The ID of the(frontend) client using this adapter. @@ -42,7 +42,6 @@ public class InitializeRequestArguments : IRequest /// /// Determines in what format paths are specified.The default is 'path', which is the native format. - /// Values: 'path', 'uri', etc. /// [Optional] public string PathFormat { get; set; } @@ -66,6 +65,11 @@ public class InitializeRequestArguments : IRequest /// Client supports memory references. /// [Optional] public bool? SupportsMemoryReferences { get; set; } + + /// + /// Client supports progress reporting. + /// + [Optional] public bool? SupportsProgressReporting { get; set; } } } diff --git a/src/Dap.Protocol/Requests/LaunchRequestArguments.cs b/src/Dap.Protocol/Requests/LaunchRequestArguments.cs index 180e0581a..4891c6e19 100644 --- a/src/Dap.Protocol/Requests/LaunchRequestArguments.cs +++ b/src/Dap.Protocol/Requests/LaunchRequestArguments.cs @@ -1,6 +1,8 @@ +using System.Collections.Generic; using Newtonsoft.Json.Linq; using OmniSharp.Extensions.DebugAdapter.Protocol.Serialization; using MediatR; +using Newtonsoft.Json; using OmniSharp.Extensions.JsonRpc; namespace OmniSharp.Extensions.DebugAdapter.Protocol.Requests @@ -20,6 +22,10 @@ public class LaunchRequestArguments : IRequest /// The client should leave the data intact. /// [Optional] + [JsonProperty(PropertyName = "__restart")] public JToken Restart { get; set; } + + [JsonExtensionData] + public IDictionary ExtensionData { get; set; } = new Dictionary(); } } diff --git a/src/Dap.Protocol/Requests/NextArguments.cs b/src/Dap.Protocol/Requests/NextArguments.cs index 5f91a75ed..a3945fbff 100644 --- a/src/Dap.Protocol/Requests/NextArguments.cs +++ b/src/Dap.Protocol/Requests/NextArguments.cs @@ -1,4 +1,6 @@ using MediatR; +using OmniSharp.Extensions.DebugAdapter.Protocol.Models; +using OmniSharp.Extensions.DebugAdapter.Protocol.Serialization; using OmniSharp.Extensions.JsonRpc; namespace OmniSharp.Extensions.DebugAdapter.Protocol.Requests @@ -10,6 +12,11 @@ public class NextArguments : IRequest /// Execute 'next' for this thread. /// public long ThreadId { get; set; } - } + /// + /// Optional granularity to step. If no granularity is specified, a granularity of 'statement' is assumed. + /// + [Optional] + public SteppingGranularity Granularity { get; set; } + } } diff --git a/src/Dap.Protocol/Requests/RequestNames.cs b/src/Dap.Protocol/Requests/RequestNames.cs index 660a50600..8cde9591f 100644 --- a/src/Dap.Protocol/Requests/RequestNames.cs +++ b/src/Dap.Protocol/Requests/RequestNames.cs @@ -9,11 +9,13 @@ public static class RequestNames public const string Restart = "restart"; public const string Disconnect = "disconnect"; public const string Terminate = "terminate"; + public const string BreakpointLocations = "breakpointLocations"; public const string SetBreakpoints = "setBreakpoints"; public const string SetFunctionBreakpoints = "setFunctionBreakpoints"; public const string SetExceptionBreakpoints = "setExceptionBreakpoints"; public const string DataBreakpointInfo = "dataBreakpointInfo"; public const string SetDataBreakpoints = "setDataBreakpoints"; + public const string SetInstructionBreakpoints = "setInstructionBreakpoints"; public const string Continue = "continue"; public const string Next = "next"; public const string StepIn = "stepIn"; @@ -42,6 +44,7 @@ public static class RequestNames public const string ReadMemory = "readMemory"; public const string Disassemble = "disassemble"; public const string RunInTerminal = "runInTerminal"; + public const string Cancel = "cancel"; } } diff --git a/src/Dap.Protocol/Requests/RunInTerminalArguments.cs b/src/Dap.Protocol/Requests/RunInTerminalArguments.cs index 6aceb3475..5d7482678 100644 --- a/src/Dap.Protocol/Requests/RunInTerminalArguments.cs +++ b/src/Dap.Protocol/Requests/RunInTerminalArguments.cs @@ -12,7 +12,7 @@ public class RunInTerminalArguments : IRequest /// /// What kind of terminal to launch. /// - [Optional] public RunInTerminalArgumentsKind Kind { get; set; } + [Optional] public RunInTerminalArgumentsKind? Kind { get; set; } /// /// Optional title of the terminal. diff --git a/src/Dap.Protocol/Requests/SetDataBreakpointsArguments.cs b/src/Dap.Protocol/Requests/SetDataBreakpointsArguments.cs index e624a659c..0bc9599cb 100644 --- a/src/Dap.Protocol/Requests/SetDataBreakpointsArguments.cs +++ b/src/Dap.Protocol/Requests/SetDataBreakpointsArguments.cs @@ -12,5 +12,4 @@ public class SetDataBreakpointsArguments : IRequest /// public Container Breakpoints { get; set; } } - } diff --git a/src/Dap.Protocol/Requests/SetDataBreakpointsResponse.cs b/src/Dap.Protocol/Requests/SetDataBreakpointsResponse.cs index ee28ed857..91b0ab2bb 100644 --- a/src/Dap.Protocol/Requests/SetDataBreakpointsResponse.cs +++ b/src/Dap.Protocol/Requests/SetDataBreakpointsResponse.cs @@ -9,5 +9,4 @@ public class SetDataBreakpointsResponse /// public Container Breakpoints { get; set; } } - } diff --git a/src/Dap.Protocol/Requests/SetInstructionBreakpointsArguments.cs b/src/Dap.Protocol/Requests/SetInstructionBreakpointsArguments.cs new file mode 100644 index 000000000..79005fcfa --- /dev/null +++ b/src/Dap.Protocol/Requests/SetInstructionBreakpointsArguments.cs @@ -0,0 +1,15 @@ +using MediatR; +using OmniSharp.Extensions.DebugAdapter.Protocol.Models; +using OmniSharp.Extensions.JsonRpc; + +namespace OmniSharp.Extensions.DebugAdapter.Protocol.Requests +{ + [Method(RequestNames.SetInstructionBreakpoints, Direction.ClientToServer)] + public class SetInstructionBreakpointsArguments : IRequest + { + /// + /// The contents of this array replaces all existing data breakpoints. An empty array clears all data breakpoints. + /// + public Container Breakpoints { get; set; } + } +} diff --git a/src/Dap.Protocol/Requests/SetInstructionBreakpointsResponse.cs b/src/Dap.Protocol/Requests/SetInstructionBreakpointsResponse.cs new file mode 100644 index 000000000..b825f7254 --- /dev/null +++ b/src/Dap.Protocol/Requests/SetInstructionBreakpointsResponse.cs @@ -0,0 +1,12 @@ +using OmniSharp.Extensions.DebugAdapter.Protocol.Models; + +namespace OmniSharp.Extensions.DebugAdapter.Protocol.Requests +{ + public class SetInstructionBreakpointsResponse + { + /// + /// Information about the data breakpoints.The array elements correspond to the elements of the input argument 'breakpoints' array. + /// + public Container Breakpoints { get; set; } + } +} diff --git a/src/Dap.Protocol/Requests/StepBackArguments.cs b/src/Dap.Protocol/Requests/StepBackArguments.cs index 78b88732e..1db30a979 100644 --- a/src/Dap.Protocol/Requests/StepBackArguments.cs +++ b/src/Dap.Protocol/Requests/StepBackArguments.cs @@ -1,4 +1,6 @@ using MediatR; +using OmniSharp.Extensions.DebugAdapter.Protocol.Models; +using OmniSharp.Extensions.DebugAdapter.Protocol.Serialization; using OmniSharp.Extensions.JsonRpc; namespace OmniSharp.Extensions.DebugAdapter.Protocol.Requests @@ -10,6 +12,12 @@ public class StepBackArguments : IRequest /// Execute 'stepBack' for this thread. /// public long ThreadId { get; set; } + + /// + /// Optional granularity to step. If no granularity is specified, a granularity of 'statement' is assumed. + /// + [Optional] + public SteppingGranularity Granularity { get; set; } } } diff --git a/src/Dap.Protocol/Requests/StepInArguments.cs b/src/Dap.Protocol/Requests/StepInArguments.cs index 94e4bc093..1f9f9b99d 100644 --- a/src/Dap.Protocol/Requests/StepInArguments.cs +++ b/src/Dap.Protocol/Requests/StepInArguments.cs @@ -1,5 +1,6 @@ using OmniSharp.Extensions.DebugAdapter.Protocol.Serialization; using MediatR; +using OmniSharp.Extensions.DebugAdapter.Protocol.Models; using OmniSharp.Extensions.JsonRpc; namespace OmniSharp.Extensions.DebugAdapter.Protocol.Requests @@ -16,6 +17,12 @@ public class StepInArguments : IRequest /// Optional id of the target to step into. /// [Optional] public long? TargetId { get; set; } + + /// + /// Optional granularity to step. If no granularity is specified, a granularity of 'statement' is assumed. + /// + [Optional] + public SteppingGranularity Granularity { get; set; } } } diff --git a/src/Dap.Protocol/Requests/StepOutArguments.cs b/src/Dap.Protocol/Requests/StepOutArguments.cs index 02db45f68..b05740326 100644 --- a/src/Dap.Protocol/Requests/StepOutArguments.cs +++ b/src/Dap.Protocol/Requests/StepOutArguments.cs @@ -1,4 +1,6 @@ using MediatR; +using OmniSharp.Extensions.DebugAdapter.Protocol.Models; +using OmniSharp.Extensions.DebugAdapter.Protocol.Serialization; using OmniSharp.Extensions.JsonRpc; namespace OmniSharp.Extensions.DebugAdapter.Protocol.Requests @@ -10,6 +12,12 @@ public class StepOutArguments : IRequest /// Execute 'stepOut' for this thread. /// public long ThreadId { get; set; } + + /// + /// Optional granularity to step. If no granularity is specified, a granularity of 'statement' is assumed. + /// + [Optional] + public SteppingGranularity Granularity { get; set; } } } diff --git a/src/Dap.Protocol/Requests/TerminateArguments.cs b/src/Dap.Protocol/Requests/TerminateArguments.cs index 196786433..5d26bcc56 100644 --- a/src/Dap.Protocol/Requests/TerminateArguments.cs +++ b/src/Dap.Protocol/Requests/TerminateArguments.cs @@ -12,5 +12,4 @@ public class TerminateArguments : IRequest /// [Optional] public bool? Restart { get; set; } } - } diff --git a/src/Dap.Protocol/Requests/VariablesArguments.cs b/src/Dap.Protocol/Requests/VariablesArguments.cs index 736520c96..4bad868e9 100644 --- a/src/Dap.Protocol/Requests/VariablesArguments.cs +++ b/src/Dap.Protocol/Requests/VariablesArguments.cs @@ -16,7 +16,7 @@ public class VariablesArguments : IRequest /// /// Optional filter to limit the child variables to either named or indexed.If ommited, both types are fetched. /// - [Optional] public VariablesArgumentsFilter Filter { get; set; } + [Optional] public VariablesArgumentsFilter? Filter { get; set; } /// /// The index of the first variable to return; if omitted children start at 0. diff --git a/src/Dap.Protocol/Serialization/ContractResolver.cs b/src/Dap.Protocol/Serialization/ContractResolver.cs index 5fa29efbe..248d45188 100644 --- a/src/Dap.Protocol/Serialization/ContractResolver.cs +++ b/src/Dap.Protocol/Serialization/ContractResolver.cs @@ -15,7 +15,7 @@ public ContractResolver() protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization) { var property = base.CreateProperty(member, memberSerialization); - if (member.GetCustomAttributes().Any() + if (member.GetCustomAttributes(true).Any() || property.DeclaringType.Name.EndsWith("Capabilities") ) { diff --git a/src/Dap.Server/Dap.Server.csproj b/src/Dap.Server/Dap.Server.csproj index 80552db1c..5c9ff16dd 100644 --- a/src/Dap.Server/Dap.Server.csproj +++ b/src/Dap.Server/Dap.Server.csproj @@ -9,8 +9,11 @@ - - + + + + + diff --git a/src/Dap.Server/DebugAdapterServer.cs b/src/Dap.Server/DebugAdapterServer.cs new file mode 100644 index 000000000..10b4fbeea --- /dev/null +++ b/src/Dap.Server/DebugAdapterServer.cs @@ -0,0 +1,266 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reactive.Disposables; +using System.Reactive.Linq; +using System.Reactive.Subjects; +using System.Reactive.Threading.Tasks; +using System.Threading; +using System.Threading.Tasks; +using MediatR; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using OmniSharp.Extensions.DebugAdapter.Protocol; +using OmniSharp.Extensions.DebugAdapter.Protocol.Events; +using OmniSharp.Extensions.DebugAdapter.Protocol.Models; +using OmniSharp.Extensions.DebugAdapter.Protocol.Requests; +using OmniSharp.Extensions.DebugAdapter.Shared; +using OmniSharp.Extensions.JsonRpc; +using IOutputHandler = OmniSharp.Extensions.JsonRpc.IOutputHandler; +using OutputHandler = OmniSharp.Extensions.JsonRpc.OutputHandler; + +namespace OmniSharp.Extensions.DebugAdapter.Server +{ + public class DebugAdapterServer : JsonRpcServerBase, IDebugAdapterServer, IInitializeHandler + { + private readonly DebugAdapterHandlerCollection _collection; + private readonly IEnumerable _initializeDelegates; + private readonly IEnumerable _initializedDelegates; + private readonly IEnumerable _startedDelegates; + private readonly CompositeDisposable _disposable = new CompositeDisposable(); + private readonly Connection _connection; + private readonly IServerProgressManager _progressManager; + private readonly DapReceiver _receiver; + private Task _initializingTask; + private readonly ISubject _initializeComplete = new AsyncSubject(); + private readonly Capabilities _capabilities; + + public static Task From(Action optionsAction) + { + return From(optionsAction, CancellationToken.None); + } + + public static Task From(DebugAdapterServerOptions options) + { + return From(options, CancellationToken.None); + } + + public static Task From(Action optionsAction, CancellationToken token) + { + var options = new DebugAdapterServerOptions(); + optionsAction(options); + return From(options, token); + } + + public static IDebugAdapterServer PreInit(Action optionsAction) + { + var options = new DebugAdapterServerOptions(); + optionsAction(options); + return PreInit(options); + } + + public static async Task From(DebugAdapterServerOptions options, CancellationToken token) + { + var server = (DebugAdapterServer) PreInit(options); + await server.Initialize(token); + + return server; + } + + /// + /// Create the server without connecting to the client + /// + /// Mainly used for unit testing + /// + /// + /// + public static IDebugAdapterServer PreInit(DebugAdapterServerOptions options) + { + return new DebugAdapterServer(options); + } + + internal DebugAdapterServer(DebugAdapterServerOptions options) : base(options) + { + var services = options.Services; + + services.AddLogging(builder => options.LoggingBuilderAction(builder)); + + _capabilities = options.Capabilities; + + var receiver = _receiver = new DapReceiver(); + var serializer = options.Serializer; + var collection = new DebugAdapterHandlerCollection(); + services.AddSingleton(collection); + _collection = collection; + _initializeDelegates = options.InitializeDelegates; + _initializedDelegates = options.InitializedDelegates; + _startedDelegates = options.StartedDelegates; + + services.AddSingleton(_ => new OutputHandler( + options.Output, + options.Serializer, + receiver.ShouldFilterOutput, + _.GetService>()) + ); + services.AddSingleton(_collection); + services.AddSingleton(serializer); + services.AddSingleton(options.RequestProcessIdentifier); + services.AddSingleton(receiver); + + services.AddSingleton(this); + services.AddSingleton(); + services.AddSingleton>(_ => _.GetRequiredService()); + services.AddSingleton(); + + services.AddSingleton(); + services.AddSingleton(_ => _.GetRequiredService() as IJsonRpcHandler); + + EnsureAllHandlersAreRegistered(); + + var serviceProvider = services.BuildServiceProvider(); + _disposable.Add(serviceProvider); + + _progressManager = serviceProvider.GetRequiredService(); + var requestRouter = serviceProvider.GetRequiredService>(); + var responseRouter = serviceProvider.GetRequiredService(); + ResponseRouter = responseRouter; + _connection = new Connection( + options.Input, + serviceProvider.GetRequiredService(), + receiver, + options.RequestProcessIdentifier, + serviceProvider.GetRequiredService>(), + responseRouter, + serviceProvider.GetRequiredService(), + options.OnUnhandledException, + options.CreateResponseException, + options.MaximumRequestTimeout, + options.SupportsContentModified, + options.Concurrency + ); + + _disposable.Add(_collection.Add(this)); + + { + var serviceHandlers = serviceProvider.GetServices().ToArray(); + _collection.Add(this); + _disposable.Add(_collection.Add(serviceHandlers)); + options.AddLinks(_collection); + } + } + + + public async Task Initialize(CancellationToken token) + { + if (_initializingTask != null) + { + try + { + await _initializingTask; + } + catch + { + // Swallow exceptions because the original initialization task will report errors if it fails (don't want to doubly report). + } + + return; + } + + _connection.Open(); + try + { + _initializingTask = _initializeComplete + .Select(result => _startedDelegates.Select(@delegate => + Observable.FromAsync(() => @delegate(this, result, token)) + ) + .ToObservable() + .Merge() + .Select(z => result) + ) + .Merge() + .LastOrDefaultAsync() + .ToTask(token); + await _initializingTask; + + this.SendInitialized(new InitializedEvent()); + } + catch (TaskCanceledException e) + { + _initializeComplete.OnError(e); + throw; + } + catch (Exception e) + { + _initializeComplete.OnError(e); + throw; + } + } + + async Task IRequestHandler.Handle(InitializeRequestArguments request, CancellationToken cancellationToken) + { + ClientSettings = request; + + await Task.WhenAll(_initializeDelegates.Select(c => c(this, request, cancellationToken))); + + _receiver.Initialized(); + + var response = new InitializeResponse() { + AdditionalModuleColumns = _capabilities.AdditionalModuleColumns, + ExceptionBreakpointFilters = _capabilities.ExceptionBreakpointFilters, + SupportedChecksumAlgorithms = _capabilities.SupportedChecksumAlgorithms, + SupportsCompletionsRequest = _capabilities.SupportsCompletionsRequest ?? _collection.ContainsHandler(typeof(ICompletionsHandler)), + SupportsConditionalBreakpoints = _capabilities.SupportsConditionalBreakpoints, + SupportsDataBreakpoints = _capabilities.SupportsDataBreakpoints ?? _collection.ContainsHandler(typeof(IDataBreakpointInfoHandler)) || _collection.ContainsHandler(typeof(ISetDataBreakpointsHandler)), + SupportsDisassembleRequest = _capabilities.SupportsDisassembleRequest ?? _collection.ContainsHandler(typeof(IDisassembleHandler)), + SupportsExceptionOptions = _capabilities.SupportsExceptionOptions, + SupportsFunctionBreakpoints = _capabilities.SupportsFunctionBreakpoints ?? _collection.ContainsHandler(typeof(ISetFunctionBreakpointsHandler)), + SupportsLogPoints = _capabilities.SupportsLogPoints, + SupportsModulesRequest = _capabilities.SupportsModulesRequest ?? _collection.ContainsHandler(typeof(IModuleHandler)), + SupportsRestartFrame = _capabilities.SupportsRestartFrame ?? _collection.ContainsHandler(typeof(IRestartFrameHandler)), + SupportsRestartRequest = _capabilities.SupportsRestartRequest ?? _collection.ContainsHandler(typeof(IRestartHandler)), + SupportsSetExpression = _capabilities.SupportsSetExpression ?? _collection.ContainsHandler(typeof(ISetExpressionHandler)), + SupportsSetVariable = _capabilities.SupportsSetVariable ?? _collection.ContainsHandler(typeof(ISetVariableHandler)), + SupportsStepBack = _capabilities.SupportsStepBack ?? _collection.ContainsHandler(typeof(IStepBackHandler)) && _collection.ContainsHandler(typeof(IReverseContinueHandler)), + SupportsTerminateRequest = _capabilities.SupportsTerminateRequest ?? _collection.ContainsHandler(typeof(ITerminateHandler)), + SupportTerminateDebuggee = _capabilities.SupportTerminateDebuggee, + SupportsConfigurationDoneRequest = _capabilities.SupportsConfigurationDoneRequest ?? _collection.ContainsHandler(typeof(IConfigurationDoneHandler)), + SupportsEvaluateForHovers = _capabilities.SupportsEvaluateForHovers, + SupportsExceptionInfoRequest = _capabilities.SupportsExceptionInfoRequest ?? _collection.ContainsHandler(typeof(IExceptionInfoHandler)), + SupportsGotoTargetsRequest = _capabilities.SupportsGotoTargetsRequest ?? _collection.ContainsHandler(typeof(IGotoTargetsHandler)), + SupportsHitConditionalBreakpoints = _capabilities.SupportsHitConditionalBreakpoints, + SupportsLoadedSourcesRequest = _capabilities.SupportsLoadedSourcesRequest ?? _collection.ContainsHandler(typeof(ILoadedSourcesHandler)), + SupportsReadMemoryRequest = _capabilities.SupportsReadMemoryRequest ?? _collection.ContainsHandler(typeof(IReadMemoryHandler)), + SupportsTerminateThreadsRequest = _capabilities.SupportsTerminateThreadsRequest ?? _collection.ContainsHandler(typeof(ITerminateThreadsHandler)), + SupportsValueFormattingOptions = _capabilities.SupportsValueFormattingOptions, + SupportsDelayedStackTraceLoading = _capabilities.SupportsDelayedStackTraceLoading, + SupportsStepInTargetsRequest = _capabilities.SupportsStepInTargetsRequest ?? _collection.ContainsHandler(typeof(IStepInTargetsHandler)), + SupportsCancelRequest = _capabilities.SupportsCancelRequest ?? _collection.ContainsHandler(typeof(ICancelHandler)), + SupportsClipboardContext = _capabilities.SupportsClipboardContext, + SupportsInstructionBreakpoints = _capabilities.SupportsInstructionBreakpoints ?? _collection.ContainsHandler(typeof(ISetInstructionBreakpointsHandler)), + SupportsSteppingGranularity = _capabilities.SupportsSteppingGranularity, + SupportsBreakpointLocationsRequest = _capabilities.SupportsBreakpointLocationsRequest ?? _collection.ContainsHandler(typeof(IBreakpointLocationsHandler)) + }; + + ServerSettings = response; + + await Task.WhenAll(_initializedDelegates.Select(c => c(this, request, response, cancellationToken))); + _initializeComplete.OnNext(response); + _initializeComplete.OnCompleted(); + + return response; + } + + protected override IResponseRouter ResponseRouter { get; } + protected override IHandlersManager HandlersManager => _collection; + public InitializeRequestArguments ClientSettings { get; private set; } + public InitializeResponse ServerSettings { get; private set; } + public IServerProgressManager ProgressManager => _progressManager; + + public void Dispose() + { + _disposable?.Dispose(); + _connection?.Dispose(); + } + } +} diff --git a/src/Dap.Server/DebugAdapterServerOptions.cs b/src/Dap.Server/DebugAdapterServerOptions.cs new file mode 100644 index 000000000..af978201d --- /dev/null +++ b/src/Dap.Server/DebugAdapterServerOptions.cs @@ -0,0 +1,106 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Newtonsoft.Json.Linq; +using OmniSharp.Extensions.DebugAdapter.Protocol; +using OmniSharp.Extensions.DebugAdapter.Protocol.Models; +using OmniSharp.Extensions.JsonRpc; + +namespace OmniSharp.Extensions.DebugAdapter.Server +{ + public class DebugAdapterServerOptions : DebugAdapterRpcOptionsBase, IDebugAdapterServerRegistry + { + public Capabilities Capabilities { get; set; } = new Capabilities(); + internal readonly List StartedDelegates = new List(); + internal readonly List InitializedDelegates = new List(); + internal readonly List InitializeDelegates = new List(); + public ISerializer Serializer { get; set; } = new DapSerializer(); + public override IRequestProcessIdentifier RequestProcessIdentifier { get; set; } = new ParallelRequestProcessIdentifier(); + + IDebugAdapterServerRegistry IJsonRpcHandlerRegistry.AddHandler(string method, IJsonRpcHandler handler, JsonRpcHandlerOptions options) => + this.AddHandler(method, handler, options); + + IDebugAdapterServerRegistry IJsonRpcHandlerRegistry.AddHandler(string method, Func handlerFunc, + JsonRpcHandlerOptions options) => this.AddHandler(method, handlerFunc, options); + + IDebugAdapterServerRegistry IJsonRpcHandlerRegistry.AddHandlers(params IJsonRpcHandler[] handlers) => this.AddHandlers(handlers); + + IDebugAdapterServerRegistry IJsonRpcHandlerRegistry.AddHandler(Func handlerFunc, + JsonRpcHandlerOptions options) => this.AddHandler(handlerFunc, options); + + IDebugAdapterServerRegistry IJsonRpcHandlerRegistry.AddHandler(THandler handler, JsonRpcHandlerOptions options) => + this.AddHandler(handler, options); + + IDebugAdapterServerRegistry IJsonRpcHandlerRegistry.AddHandler(JsonRpcHandlerOptions options) => + this.AddHandler(options); + + IDebugAdapterServerRegistry IJsonRpcHandlerRegistry.AddHandler(string method, JsonRpcHandlerOptions options) => + this.AddHandler(method, options); + + IDebugAdapterServerRegistry IJsonRpcHandlerRegistry.AddHandler(Type type, JsonRpcHandlerOptions options) => this.AddHandler(type, options); + + IDebugAdapterServerRegistry IJsonRpcHandlerRegistry.AddHandler(string method, Type type, JsonRpcHandlerOptions options) => + this.AddHandler(method, type, options); + + IDebugAdapterServerRegistry IJsonRpcHandlerRegistry.OnJsonRequest(string method, Func> handler, + JsonRpcHandlerOptions options) => OnJsonRequest(method, handler, options); + + IDebugAdapterServerRegistry IJsonRpcHandlerRegistry.OnJsonRequest(string method, Func> handler, + JsonRpcHandlerOptions options) => OnJsonRequest(method, handler, options); + + IDebugAdapterServerRegistry IJsonRpcHandlerRegistry.OnRequest(string method, Func> handler, + JsonRpcHandlerOptions options) => OnRequest(method, handler, options); + + IDebugAdapterServerRegistry IJsonRpcHandlerRegistry.OnRequest(string method, + Func> handler, JsonRpcHandlerOptions options) => OnRequest(method, handler, options); + + IDebugAdapterServerRegistry IJsonRpcHandlerRegistry.OnRequest(string method, Func> handler, + JsonRpcHandlerOptions options) => OnRequest(method, handler, options); + + IDebugAdapterServerRegistry IJsonRpcHandlerRegistry.OnRequest(string method, Func> handler, + JsonRpcHandlerOptions options) => OnRequest(method, handler, options); + + IDebugAdapterServerRegistry IJsonRpcHandlerRegistry.OnRequest(string method, Func handler, + JsonRpcHandlerOptions options) => OnRequest(method, handler, options); + + IDebugAdapterServerRegistry IJsonRpcHandlerRegistry.OnRequest(string method, Func handler, + JsonRpcHandlerOptions options) => OnRequest(method, handler, options); + + IDebugAdapterServerRegistry IJsonRpcHandlerRegistry.OnRequest(string method, Func handler, + JsonRpcHandlerOptions options) => OnRequest(method, handler, options); + + IDebugAdapterServerRegistry IJsonRpcHandlerRegistry.OnNotification(string method, Action handler, + JsonRpcHandlerOptions options) => OnNotification(method, handler, options); + + IDebugAdapterServerRegistry IJsonRpcHandlerRegistry.OnJsonNotification(string method, Action handler, JsonRpcHandlerOptions options) => + OnJsonNotification(method, handler, options); + + IDebugAdapterServerRegistry IJsonRpcHandlerRegistry.OnJsonNotification(string method, Func handler, + JsonRpcHandlerOptions options) => OnJsonNotification(method, handler, options); + + IDebugAdapterServerRegistry IJsonRpcHandlerRegistry.OnJsonNotification(string method, Func handler, + JsonRpcHandlerOptions options) => OnJsonNotification(method, handler, options); + + IDebugAdapterServerRegistry IJsonRpcHandlerRegistry.OnJsonNotification(string method, Action handler, + JsonRpcHandlerOptions options) => OnJsonNotification(method, handler, options); + + IDebugAdapterServerRegistry IJsonRpcHandlerRegistry.OnNotification(string method, Action handler, + JsonRpcHandlerOptions options) => OnNotification(method, handler, options); + + IDebugAdapterServerRegistry IJsonRpcHandlerRegistry.OnNotification(string method, Func handler, + JsonRpcHandlerOptions options) => OnNotification(method, handler, options); + + IDebugAdapterServerRegistry IJsonRpcHandlerRegistry.OnNotification(string method, Func handler, + JsonRpcHandlerOptions options) => OnNotification(method, handler, options); + + IDebugAdapterServerRegistry IJsonRpcHandlerRegistry.OnNotification(string method, Action handler, JsonRpcHandlerOptions options) => + OnNotification(method, handler, options); + + IDebugAdapterServerRegistry IJsonRpcHandlerRegistry.OnNotification(string method, Func handler, + JsonRpcHandlerOptions options) => OnNotification(method, handler, options); + + IDebugAdapterServerRegistry IJsonRpcHandlerRegistry.OnNotification(string method, Func handler, JsonRpcHandlerOptions options) => + OnNotification(method, handler, options); + } +} \ No newline at end of file diff --git a/src/Dap.Server/DebugAdapterServerOptionsExtensions.cs b/src/Dap.Server/DebugAdapterServerOptionsExtensions.cs new file mode 100644 index 000000000..4b097287d --- /dev/null +++ b/src/Dap.Server/DebugAdapterServerOptionsExtensions.cs @@ -0,0 +1,67 @@ +using System; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using OmniSharp.Extensions.DebugAdapter.Protocol; +using OmniSharp.Extensions.JsonRpc; + +namespace OmniSharp.Extensions.DebugAdapter.Server +{ + public static class DebugAdapterServerOptionsExtensions + { + public static DebugAdapterServerOptions WithSerializer(this DebugAdapterServerOptions options, ISerializer serializer) + { + options.Serializer = serializer; + return options; + } + + public static DebugAdapterServerOptions WithRequestProcessIdentifier(this DebugAdapterServerOptions options, IRequestProcessIdentifier requestProcessIdentifier) + { + options.RequestProcessIdentifier = requestProcessIdentifier; + return options; + } + + public static DebugAdapterServerOptions WithServices(this DebugAdapterServerOptions options, Action servicesAction) + { + servicesAction(options.Services); + return options; + } + + public static DebugAdapterServerOptions OnInitialize(this DebugAdapterServerOptions options, InitializeDelegate @delegate) + { + options.InitializeDelegates.Add(@delegate); + return options; + } + + + public static DebugAdapterServerOptions OnInitialized(this DebugAdapterServerOptions options, InitializedDelegate @delegate) + { + options.InitializedDelegates.Add(@delegate); + return options; + } + + public static DebugAdapterServerOptions OnStarted(this DebugAdapterServerOptions options, OnServerStartedDelegate @delegate) + { + options.StartedDelegates.Add(@delegate); + return options; + } + + public static DebugAdapterServerOptions ConfigureLogging(this DebugAdapterServerOptions options, Action builderAction) + { + options.LoggingBuilderAction = builderAction; + return options; + } + + public static DebugAdapterServerOptions AddDefaultLoggingProvider(this DebugAdapterServerOptions options) + { + options.AddDefaultLoggingProvider = true; + return options; + } + + public static DebugAdapterServerOptions ConfigureConfiguration(this DebugAdapterServerOptions options, Action builderAction) + { + options.ConfigurationBuilderAction = builderAction; + return options; + } + } +} \ No newline at end of file diff --git a/src/Dap.Server/InitializeDelegate.cs b/src/Dap.Server/InitializeDelegate.cs new file mode 100644 index 000000000..b5ca538d1 --- /dev/null +++ b/src/Dap.Server/InitializeDelegate.cs @@ -0,0 +1,9 @@ +using System.Threading; +using System.Threading.Tasks; +using OmniSharp.Extensions.DebugAdapter.Protocol; +using OmniSharp.Extensions.DebugAdapter.Protocol.Requests; + +namespace OmniSharp.Extensions.DebugAdapter.Server +{ + public delegate Task InitializeDelegate(IDebugAdapterServer server, InitializeRequestArguments request, CancellationToken cancellationToken); +} \ No newline at end of file diff --git a/src/Dap.Server/InitializedDelegate.cs b/src/Dap.Server/InitializedDelegate.cs new file mode 100644 index 000000000..dad9ec28a --- /dev/null +++ b/src/Dap.Server/InitializedDelegate.cs @@ -0,0 +1,9 @@ +using System.Threading; +using System.Threading.Tasks; +using OmniSharp.Extensions.DebugAdapter.Protocol; +using OmniSharp.Extensions.DebugAdapter.Protocol.Requests; + +namespace OmniSharp.Extensions.DebugAdapter.Server +{ + public delegate Task InitializedDelegate(IDebugAdapterServer server, InitializeRequestArguments request, InitializeResponse response, CancellationToken cancellationToken); +} \ No newline at end of file diff --git a/src/Dap.Server/ProgressObserver.cs b/src/Dap.Server/ProgressObserver.cs new file mode 100644 index 000000000..7c2d3f46d --- /dev/null +++ b/src/Dap.Server/ProgressObserver.cs @@ -0,0 +1,83 @@ +using System; +using System.Collections.Generic; +using System.Reactive.Disposables; +using System.Threading; +using OmniSharp.Extensions.DebugAdapter.Protocol.Events; +using OmniSharp.Extensions.DebugAdapter.Protocol.Models; +using OmniSharp.Extensions.JsonRpc; + +namespace OmniSharp.Extensions.DebugAdapter.Server +{ + class ProgressObserver : IProgressObserver + { + private readonly ProgressToken _progressToken; + private readonly IResponseRouter _router; + private readonly Func _onError; + private readonly Func _onComplete; + private readonly CompositeDisposable _disposable; + + public ProgressObserver( + IResponseRouter router, + ProgressStartEvent begin, + Func onError, + Func onComplete, + CancellationToken cancellationToken) + { + _progressToken = begin.ProgressId; + _router = router; + _onError = onError; + _onComplete = onComplete; + _disposable = new CompositeDisposable {Disposable.Create(OnCompleted)}; + cancellationToken.Register(Dispose); + _router.SendNotification(begin); + } + + public void OnCompleted() + { + if (_disposable.IsDisposed) return; + var @event = _onComplete?.Invoke() ?? new ProgressEndEvent() {Message = "", ProgressId = _progressToken}; + if (EqualityComparer.Default.Equals(@event.ProgressId, default)) + { + @event.ProgressId = _progressToken; + } + + _router.SendNotification(@event); + } + + void IObserver.OnError(Exception error) + { + if (_disposable.IsDisposed) return; + var @event = _onError?.Invoke(error) ?? new ProgressEndEvent() {Message = error.ToString(), ProgressId = _progressToken}; + if (EqualityComparer.Default.Equals(@event.ProgressId, default)) + { + @event.ProgressId = _progressToken; + } + + _router.SendNotification(@event); + } + + public void OnNext(ProgressUpdateEvent value) + { + if (_disposable.IsDisposed) return; + if (EqualityComparer.Default.Equals(value.ProgressId, default)) + { + value.ProgressId = _progressToken; + } + + _router.SendNotification(value); + } + + public ProgressToken ProgressId => _progressToken; + + public void OnNext(string message, double? percentage) + { + OnNext(new ProgressUpdateEvent() { + ProgressId = _progressToken, + Message = message, + Percentage = percentage + }); + } + + public void Dispose() => _disposable?.Dispose(); + } +} diff --git a/src/Dap.Server/ServerProgressManager.cs b/src/Dap.Server/ServerProgressManager.cs new file mode 100644 index 000000000..cb2455fb4 --- /dev/null +++ b/src/Dap.Server/ServerProgressManager.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using OmniSharp.Extensions.DebugAdapter.Protocol.Events; +using OmniSharp.Extensions.DebugAdapter.Protocol.Models; +using OmniSharp.Extensions.DebugAdapter.Protocol.Requests; +using OmniSharp.Extensions.JsonRpc; + +namespace OmniSharp.Extensions.DebugAdapter.Server +{ + public class ServerProgressManager : ICancelHandler, IServerProgressManager + { + private readonly IResponseRouter _router; + private readonly ISerializer _serializer; + + private readonly ConcurrentDictionary _activeObserverTokens + = new ConcurrentDictionary(EqualityComparer.Default); + + private readonly ConcurrentDictionary _activeObservers = + new ConcurrentDictionary(EqualityComparer.Default); + + public ServerProgressManager(IResponseRouter router, ISerializer serializer) + { + _router = router; + _serializer = serializer; + } + + public IProgressObserver Create(ProgressStartEvent begin, Func onError = null, Func onComplete = null) + { + if (EqualityComparer.Default.Equals(begin.ProgressId, default)) + { + begin.ProgressId = new ProgressToken(Guid.NewGuid().ToString()); + } + + if (_activeObservers.TryGetValue(begin.ProgressId, out var item)) + { + return item; + } + + onError ??= error => new ProgressEndEvent() { + Message = error.ToString() + }; + + onComplete ??= () => new ProgressEndEvent(); + + var cts = new CancellationTokenSource(); + var observer = new ProgressObserver( + _router, + begin, + onError, + onComplete, + cts.Token + ); + _activeObservers.TryAdd(observer.ProgressId, observer); + _activeObserverTokens.TryAdd(observer.ProgressId, cts); + + return observer; + } + + public Task Handle(CancelArguments request, CancellationToken cancellationToken) + { + if (request.ProgressId.HasValue && _activeObserverTokens.TryGetValue(request.ProgressId.Value, out var cts)) + { + cts.Cancel(); + } + + return Task.FromResult(new CancelResponse()); + } + } +} diff --git a/src/Dap.Shared/Dap.Shared.csproj b/src/Dap.Shared/Dap.Shared.csproj new file mode 100644 index 000000000..4503d56e4 --- /dev/null +++ b/src/Dap.Shared/Dap.Shared.csproj @@ -0,0 +1,21 @@ + + + + netstandard2.1;netstandard2.0 + AnyCPU + OmniSharp.Extensions.DebugAdapter.Shared + OmniSharp.Extensions.DebugAdapter.Shared + + + + + + + <_Parameter1>OmniSharp.Extensions.DebugAdapter.Server, PublicKey=0024000004800000940000000602000000240000525341310004000001000100391db875e68eb4bfef49ce14313b9e13f2cd3cc89eb273bbe6c11a55044c7d4f566cf092e1c77ef9e7c75b1496ae7f95d925938f5a01793dd8d9f99ae0a7595779b71b971287d7d7b5960d052078d14f5ce1a85ea5c9fb2f59ac735ff7bc215cab469b7c3486006860bad6f4c3b5204ea2f28dd4e1d05e2cca462cfd593b9f9f + + + <_Parameter1>OmniSharp.Extensions.DebugAdapter.Client, PublicKey=0024000004800000940000000602000000240000525341310004000001000100391db875e68eb4bfef49ce14313b9e13f2cd3cc89eb273bbe6c11a55044c7d4f566cf092e1c77ef9e7c75b1496ae7f95d925938f5a01793dd8d9f99ae0a7595779b71b971287d7d7b5960d052078d14f5ce1a85ea5c9fb2f59ac735ff7bc215cab469b7c3486006860bad6f4c3b5204ea2f28dd4e1d05e2cca462cfd593b9f9f + + + + diff --git a/src/Dap.Shared/DapResponseRouter.cs b/src/Dap.Shared/DapResponseRouter.cs new file mode 100644 index 000000000..e282d43cd --- /dev/null +++ b/src/Dap.Shared/DapResponseRouter.cs @@ -0,0 +1,146 @@ +using System; +using System.Collections.Concurrent; +using System.Linq; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using MediatR; +using Newtonsoft.Json.Linq; +using OmniSharp.Extensions.DebugAdapter.Protocol.Requests; +using OmniSharp.Extensions.JsonRpc; +using OmniSharp.Extensions.JsonRpc.Client; + +namespace OmniSharp.Extensions.DebugAdapter.Shared +{ + public class DapResponseRouter : IResponseRouter + { + internal readonly IOutputHandler OutputHandler; + internal readonly ISerializer Serializer; + + internal readonly ConcurrentDictionary pendingTask)> Requests = + new ConcurrentDictionary pendingTask)>(); + + internal static readonly ConcurrentDictionary MethodCache = + new ConcurrentDictionary(); + + public DapResponseRouter(IOutputHandler outputHandler, ISerializer serializer) + { + OutputHandler = outputHandler; + Serializer = serializer; + } + + public void SendNotification(string method) + { + OutputHandler.Send(new OutgoingNotification() { + Method = method + }); + } + + public void SendNotification(string method, T @params) + { + OutputHandler.Send(new OutgoingNotification() { + Method = method, + Params = @params + }); + } + + public void SendNotification(IRequest @params) + { + SendNotification(GetMethodName(@params.GetType()), @params); + } + + public Task SendRequest(IRequest @params, CancellationToken cancellationToken) + { + return SendRequest(GetMethodName(@params.GetType()), @params).Returning(cancellationToken); + } + + public IResponseRouterReturns SendRequest(string method) + { + return new ResponseRouterReturnsImpl(this, method, new object()); + } + + public IResponseRouterReturns SendRequest(string method, T @params) + { + return new ResponseRouterReturnsImpl(this, method, @params); + } + + public (string method, TaskCompletionSource pendingTask) GetRequest(long id) + { + Requests.TryGetValue(id, out var source); + return source; + } + + private string GetMethodName(Type type) + { + if (!MethodCache.TryGetValue(type, out var methodName)) + { + var attribute = MethodAttribute.From(type); + if (attribute == null) + { + throw new NotSupportedException($"Unable to infer method name for type {type.FullName}"); + } + + methodName = attribute.Method; + MethodCache.TryAdd(type, methodName); + } + + return methodName; + } + + class ResponseRouterReturnsImpl : IResponseRouterReturns + { + private readonly DapResponseRouter _router; + private readonly string _method; + private readonly object _params; + + public ResponseRouterReturnsImpl(DapResponseRouter router, string method, object @params) + { + _router = router; + _method = method; + _params = @params; + } + + public async Task Returning(CancellationToken cancellationToken) + { + var nextId = _router.Serializer.GetNextId(); + var tcs = new TaskCompletionSource(); + _router.Requests.TryAdd(nextId, (_method, tcs)); + + cancellationToken.ThrowIfCancellationRequested(); + + _router.OutputHandler.Send(new OutgoingRequest() { + Method = _method, + Params = _params, + Id = nextId + }); + if (_method != RequestNames.Cancel) + { + cancellationToken.Register(() => { + if (tcs.Task.IsCompleted) return; + _router.SendRequest(RequestNames.Cancel, new { requestId = nextId }).Returning(CancellationToken.None); + }); + } + + try + { + var result = await tcs.Task; + if (typeof(TResponse) == typeof(Unit)) + { + return (TResponse) (object) Unit.Value; + } + + return result.ToObject(_router.Serializer.JsonSerializer); + } + finally + { + _router.Requests.TryRemove(nextId, out _); + } + } + + public async Task ReturningVoid(CancellationToken cancellationToken) + { + await Returning(cancellationToken); + } + } + } +} diff --git a/src/Dap.Shared/DebugAdapterHandlerCollection.cs b/src/Dap.Shared/DebugAdapterHandlerCollection.cs new file mode 100644 index 000000000..71feb9fd6 --- /dev/null +++ b/src/Dap.Shared/DebugAdapterHandlerCollection.cs @@ -0,0 +1,166 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Reactive.Disposables; +using System.Reflection; +using MediatR; +using OmniSharp.Extensions.JsonRpc; + +namespace OmniSharp.Extensions.DebugAdapter.Shared +{ + class DebugAdapterHandlerCollection : IEnumerable, IHandlersManager + { + internal readonly HashSet _handlers = new HashSet(); + + public IEnumerator GetEnumerator() + { + return _handlers.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + IDisposable IHandlersManager.Add(IJsonRpcHandler handler, JsonRpcHandlerOptions options) => Add(new[] {handler}, options); + + IDisposable IHandlersManager.Add(string method, IJsonRpcHandler handler, JsonRpcHandlerOptions options) => Add(method, handler, options); + + IDisposable IHandlersManager.AddLink(string sourceMethod, string destinationMethod) + { + var source = _handlers.First(z => z.Method == sourceMethod); + HandlerDescriptor descriptor = null; + descriptor = GetDescriptor( + destinationMethod, + source.HandlerType, + source.Handler, + source.RequestProcessType.HasValue ? new JsonRpcHandlerOptions() {RequestProcessType = source.RequestProcessType.Value} : null, + source.TypeDescriptor, + source.HandlerType); + _handlers.Add(descriptor); + + return descriptor; + } + + public IDisposable Add(string method, IJsonRpcHandler handler, JsonRpcHandlerOptions options) + { + var descriptor = GetDescriptor(method, handler.GetType(), handler, options); + _handlers.Add(descriptor); + return new CompositeDisposable {descriptor}; + } + + public IDisposable Add(params IJsonRpcHandler[] handlers) + { + var cd = new CompositeDisposable(); + foreach (var handler in handlers) + { + if (cd.Any(z => Equals(z, handler))) continue; + + foreach (var (method, implementedInterface) in handler.GetType().GetTypeInfo() + .ImplementedInterfaces + .Select(x => (method: HandlerTypeDescriptorHelper.GetMethodName(x), implementedInterface: x)) + .Distinct(new EqualityComparer()) + .Where(x => !string.IsNullOrWhiteSpace(x.method)) + ) + { + var descriptor = GetDescriptor(method, implementedInterface, handler, null); + cd.Add(descriptor); + _handlers.Add(descriptor); + } + } + + return cd; + } + + class EqualityComparer : IEqualityComparer<(string method, Type implementedInterface)> + { + public bool Equals((string method, Type implementedInterface) x, (string method, Type implementedInterface) y) + { + return x.method?.Equals(y.method) == true; + } + + public int GetHashCode((string method, Type implementedInterface) obj) + { + return obj.method?.GetHashCode() ?? 0; + } + } + + private IDisposable Add(IJsonRpcHandler[] handlers, JsonRpcHandlerOptions options) + { + var cd = new CompositeDisposable(); + foreach (var handler in handlers) + { + if (cd.Any(z => Equals(z, handler))) continue; + + foreach (var (method, implementedInterface) in handler.GetType().GetTypeInfo() + .ImplementedInterfaces + .Select(x => (method: HandlerTypeDescriptorHelper.GetMethodName(x), implementedInterface: x)) + .Where(x => !string.IsNullOrWhiteSpace(x.method))) + { + var descriptor = GetDescriptor(method, implementedInterface, handler, options); + cd.Add(descriptor); + _handlers.Add(descriptor); + } + } + + return cd; + } + + private HandlerDescriptor GetDescriptor(string method, Type handlerType, IJsonRpcHandler handler, JsonRpcHandlerOptions options) + { + var typeDescriptor = HandlerTypeDescriptorHelper.GetHandlerTypeDescriptor(method); + var @interface = HandlerTypeDescriptorHelper.GetHandlerInterface(handlerType); + + return GetDescriptor(method, handlerType, handler, options, typeDescriptor, @interface); + } + + private HandlerDescriptor GetDescriptor(string method, Type handlerType, IJsonRpcHandler handler, JsonRpcHandlerOptions options, + IHandlerTypeDescriptor typeDescriptor, + Type @interface) + { + Type @params = null; + Type response = null; + if (@interface.GetTypeInfo().IsGenericType) + { + @params = @interface.GetTypeInfo().GetGenericArguments()[0]; + var requestInterface = @params.GetInterfaces() + .FirstOrDefault(x => x.IsGenericType && x.GetGenericTypeDefinition() == typeof(IRequest<>)); + if (requestInterface != null) + { + response = requestInterface.GetGenericArguments()[0]; + } + } + + var requestProcessType = + options?.RequestProcessType ?? + typeDescriptor?.RequestProcessType ?? + handlerType.GetCustomAttributes(true) + .Concat(@interface.GetCustomAttributes(true)) + .OfType() + .FirstOrDefault()?.Type; + + var descriptor = new HandlerDescriptor( + method, + typeDescriptor, + handler, + @interface, + @params, + response, + requestProcessType, + () => { _handlers.RemoveWhere(d => d.Handler == handler); }); + + return descriptor; + } + + public bool ContainsHandler(Type type) + { + return ContainsHandler(type.GetTypeInfo()); + } + + public bool ContainsHandler(TypeInfo typeInfo) + { + return this.Any(z => z.HandlerType.GetTypeInfo().IsAssignableFrom(typeInfo) || z.ImplementationType.GetTypeInfo().IsAssignableFrom(typeInfo)); + } + } +} diff --git a/src/Dap.Shared/DebugAdapterRequestRouter.cs b/src/Dap.Shared/DebugAdapterRequestRouter.cs new file mode 100644 index 000000000..5eb89e01c --- /dev/null +++ b/src/Dap.Shared/DebugAdapterRequestRouter.cs @@ -0,0 +1,41 @@ +using System; +using System.Linq; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using OmniSharp.Extensions.JsonRpc; +using OmniSharp.Extensions.JsonRpc.Server; + +namespace OmniSharp.Extensions.DebugAdapter.Shared +{ + internal class DebugAdapterRequestRouter : RequestRouterBase + { + private readonly DebugAdapterHandlerCollection _collection; + + + public DebugAdapterRequestRouter(DebugAdapterHandlerCollection collection, ISerializer serializer, IServiceProvider serviceProvider, IServiceScopeFactory serviceScopeFactory, ILoggerFactory loggerFactory) + : base(serializer, serviceProvider, serviceScopeFactory, loggerFactory.CreateLogger()) + { + _collection = collection; + } + + public IDisposable Add(IJsonRpcHandler handler) + { + return _collection.Add(handler); + } + + private IRequestDescriptor FindDescriptor(IMethodWithParams instance) + { + return new RequestDescriptor( _collection.Where(x => x.Method == instance.Method)); + } + + public override IRequestDescriptor GetDescriptors(Notification notification) + { + return FindDescriptor(notification); + } + + public override IRequestDescriptor GetDescriptors(Request request) + { + return FindDescriptor(request); + } + } +} diff --git a/src/Dap.Shared/HandlerDescriptor.cs b/src/Dap.Shared/HandlerDescriptor.cs new file mode 100644 index 000000000..20e11beb5 --- /dev/null +++ b/src/Dap.Shared/HandlerDescriptor.cs @@ -0,0 +1,62 @@ +using System; +using System.Diagnostics; +using System.Linq; +using MediatR; +using Microsoft.Extensions.DependencyInjection; +using OmniSharp.Extensions.JsonRpc; + +namespace OmniSharp.Extensions.DebugAdapter.Shared +{ + [DebuggerDisplay("{Method}")] + internal class HandlerDescriptor : IHandlerDescriptor, IDisposable + { + private readonly Action _disposeAction; + + public HandlerDescriptor(string method, IHandlerTypeDescriptor typeDescriptor, IJsonRpcHandler handler, Type handlerInterface, Type @params, Type response, + RequestProcessType? requestProcessType, Action disposeAction) + { + _disposeAction = disposeAction; + Handler = handler; + ImplementationType = handler.GetType(); + Method = method; + TypeDescriptor = typeDescriptor; + HandlerType = handlerInterface; + Params = @params; + Response = response; + HasReturnType = HandlerType.GetInterfaces().Any(@interface => + @interface.IsGenericType && + typeof(IRequestHandler<,>).IsAssignableFrom(@interface.GetGenericTypeDefinition()) + ); + + IsDelegatingHandler = @params?.IsGenericType == true && + ( + typeof(DelegatingRequest<>).IsAssignableFrom(@params.GetGenericTypeDefinition()) || + typeof(DelegatingNotification<>).IsAssignableFrom(@params.GetGenericTypeDefinition()) + ); + + IsNotification = typeof(IJsonRpcNotificationHandler).IsAssignableFrom(handlerInterface) || handlerInterface + .GetInterfaces().Any(z => + z.IsGenericType && typeof(IJsonRpcNotificationHandler<>).IsAssignableFrom(z.GetGenericTypeDefinition())); + IsRequest = !IsNotification; + RequestProcessType = requestProcessType; + } + + public IJsonRpcHandler Handler { get; } + public bool IsNotification { get; } + public bool IsRequest { get; } + public Type HandlerType { get; } + public Type ImplementationType { get; } + public string Method { get; } + public IHandlerTypeDescriptor TypeDescriptor { get; } + public Type Params { get; } + public Type Response { get; } + public bool HasReturnType { get; } + public bool IsDelegatingHandler { get; } + public RequestProcessType? RequestProcessType { get; } + + public void Dispose() + { + _disposeAction(); + } + } +} diff --git a/src/Dap.Testing/LanguageProtocolTestBase.cs b/src/Dap.Testing/LanguageProtocolTestBase.cs new file mode 100644 index 000000000..b104e3afb --- /dev/null +++ b/src/Dap.Testing/LanguageProtocolTestBase.cs @@ -0,0 +1,84 @@ +using System; +using System.IO.Pipelines; +using System.Reactive.Linq; +using System.Reactive.Threading.Tasks; +using System.Threading.Tasks; +using MediatR; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Logging; +using OmniSharp.Extensions.DebugAdapter.Client; +using OmniSharp.Extensions.DebugAdapter.Protocol; +using OmniSharp.Extensions.DebugAdapter.Server; +using OmniSharp.Extensions.JsonRpc.Testing; + +namespace OmniSharp.Extensions.DebugAdapter.Testing +{ + /// + /// This is a test class that is designed to allow you configure an in memory lsp client and server to do testing of handlers or behaviors. + /// + public abstract class DebugAdapterProtocolTestBase : JsonRpcTestBase + { + private IDebugAdapterClient _client; + private IDebugAdapterServer _server; + + public DebugAdapterProtocolTestBase(JsonRpcTestOptions testOptions) : base(testOptions) + { + } + + protected virtual void ConfigureClientInputOutput(PipeReader serverOutput, PipeWriter clientInput, DebugAdapterClientOptions options) + { + options.WithInput(serverOutput).WithOutput(clientInput); + } + + protected virtual void ConfigureServerInputOutput(PipeReader clientOutput, PipeWriter serverInput, DebugAdapterServerOptions options) + { + options.WithInput(clientOutput).WithOutput(serverInput); + } + + protected virtual async Task<(IDebugAdapterClient client, IDebugAdapterServer server)> Initialize( + Action clientOptionsAction, + Action serverOptionsAction) + { + var clientPipe = new Pipe(TestOptions.DefaultPipeOptions); + var serverPipe = new Pipe(TestOptions.DefaultPipeOptions); + + _client = DebugAdapterClient.PreInit(options => { + options + .WithLoggerFactory(TestOptions.ClientLoggerFactory) + .ConfigureLogging(x => { + x.Services.RemoveAll(typeof(ILoggerFactory)); + x.Services.AddSingleton(TestOptions.ClientLoggerFactory); + }) + .Services + .AddTransient(typeof(IPipelineBehavior<,>), typeof(SettlePipeline<,>)) + .AddSingleton(ServerEvents as IRequestSettler); + ConfigureClientInputOutput(serverPipe.Reader, clientPipe.Writer, options); + clientOptionsAction(options); + }); + + _server = DebugAdapterServer.PreInit(options => { + options + .WithLoggerFactory(TestOptions.ServerLoggerFactory) + .ConfigureLogging(x => { + x.Services.RemoveAll(typeof(ILoggerFactory)); + x.Services.AddSingleton(TestOptions.ServerLoggerFactory); + }) + .Services + .AddTransient(typeof(IPipelineBehavior<,>), typeof(SettlePipeline<,>)) + .AddSingleton(ServerEvents as IRequestSettler); + ConfigureServerInputOutput(clientPipe.Reader, serverPipe.Writer, options); + serverOptionsAction(options); + }); + + Disposable.Add(_client); + Disposable.Add(_server); + + return await ObservableEx.ForkJoin( + Observable.FromAsync(_client.Initialize), + Observable.FromAsync(_server.Initialize), + (a, b) => (_client, _server) + ).ToTask(CancellationToken); + } + } +} diff --git a/src/Dap.Testing/LanguageServerTestBase.cs b/src/Dap.Testing/LanguageServerTestBase.cs new file mode 100644 index 000000000..0b47ce918 --- /dev/null +++ b/src/Dap.Testing/LanguageServerTestBase.cs @@ -0,0 +1,50 @@ +using System; +using System.IO; +using System.Threading.Tasks; +using MediatR; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using OmniSharp.Extensions.DebugAdapter.Client; +using OmniSharp.Extensions.DebugAdapter.Protocol; +using OmniSharp.Extensions.JsonRpc.Testing; + +namespace OmniSharp.Extensions.DebugAdapter.Testing +{ + /// + /// This is a test class that is designed to allow you configure an in memory lsp client and and your server configuration to do integration tests against a server + /// + public abstract class LanguageServerTestBase : JsonRpcTestBase + { + private IDebugAdapterClient _client; + + public LanguageServerTestBase(JsonRpcTestOptions jsonRpcTestOptions) : base(jsonRpcTestOptions) + { + } + + protected abstract (Stream clientOutput, Stream serverInput) SetupServer(); + + protected virtual async Task InitializeClient(Action clientOptionsAction = null) + { + _client = DebugAdapterClient.PreInit(options => { + var (reader, writer) = SetupServer(); + options + .WithInput(reader) + .WithOutput(writer) + .ConfigureLogging(x => { + x.SetMinimumLevel(LogLevel.Trace); + x.Services.AddSingleton(TestOptions.ClientLoggerFactory); + }) + .Services + .AddTransient(typeof(IPipelineBehavior<,>), typeof(SettlePipeline<,>)) + .AddSingleton(ServerEvents as IRequestSettler); + clientOptionsAction?.Invoke(options); + }); + + Disposable.Add(_client); + + await _client.Initialize(CancellationToken); + + return _client; + } + } +} diff --git a/src/JsonRpc.Generators/GenerateHandlerMethodsGenerator.cs b/src/JsonRpc.Generators/GenerateHandlerMethodsGenerator.cs index f186184fd..4eaac5b50 100644 --- a/src/JsonRpc.Generators/GenerateHandlerMethodsGenerator.cs +++ b/src/JsonRpc.Generators/GenerateHandlerMethodsGenerator.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Buffers; using System.Collections; using System.Collections.Generic; @@ -203,6 +203,11 @@ IEnumerable HandleRequest( .WithModifiers(TokenList(Token(SyntaxKind.ThisKeyword))) })); + var allowDerivedRequests = _attributeData.NamedArguments + .Where(z => z.Key == "AllowDerivedRequests") + .Select(z => z.Value.Value) + .FirstOrDefault() is bool b && b; + if (registrationOptions == null) { @@ -224,6 +229,25 @@ MemberDeclarationSyntax MakeAction(TypeSyntax syntax) yield return MakeAction(CreateAsyncFunc(responseType, false, requestType)); yield return MakeAction(CreateAsyncFunc(responseType, true, requestType)); + + if (allowDerivedRequests) + { + MemberDeclarationSyntax MakeDerivedAction(TypeSyntax syntax) + { + return method + .WithParameterList(parameters.WithParameters(SeparatedList(parameters.Parameters.Concat( + new[] {Parameter(Identifier("handler")).WithType(syntax)})))) + .WithTypeParameterList(TypeParameterList(SingletonSeparatedList(TypeParameter(Identifier("T"))))) + .WithConstraintClauses(SingletonList(TypeParameterConstraintClause(IdentifierName("T")) + .WithConstraints(SingletonSeparatedList(TypeConstraint(ResolveTypeName(requestType))))) + ) + .NormalizeWhitespace(); + } + + yield return MakeDerivedAction(CreateDerivedAsyncFunc(responseType, false)); + yield return MakeDerivedAction(CreateDerivedAsyncFunc(responseType, true)); + } + if (partialItems != null) { var partialTypeSyntax = ResolveTypeName(partialItems); @@ -290,6 +314,24 @@ MemberDeclarationSyntax MakeAction(TypeSyntax syntax) yield return MakeAction(CreateAsyncFunc(responseType, false, requestType)); yield return MakeAction(CreateAsyncFunc(responseType, true, requestType)); + if (allowDerivedRequests) + { + MemberDeclarationSyntax MakeDerivedAction(TypeSyntax syntax) + { + return method + .WithParameterList(parameters.WithParameters(SeparatedList(parameters.Parameters.Concat( + new[] {Parameter(Identifier("handler")).WithType(syntax), registrationParameter})))) + .WithTypeParameterList(TypeParameterList(SingletonSeparatedList(TypeParameter(Identifier("T"))))) + .WithConstraintClauses(SingletonList(TypeParameterConstraintClause(IdentifierName("T")) + .WithConstraints(SingletonSeparatedList(TypeConstraint(ResolveTypeName(requestType))))) + ) + .NormalizeWhitespace(); + } + + yield return MakeDerivedAction(CreateDerivedAsyncFunc(responseType, false)); + yield return MakeDerivedAction(CreateDerivedAsyncFunc(responseType, true)); + } + if (partialItems != null) { var partialTypeSyntax = ResolveTypeName(partialItems); diff --git a/src/JsonRpc.Generators/Helpers.cs b/src/JsonRpc.Generators/Helpers.cs index e404bb301..35f43eeec 100644 --- a/src/JsonRpc.Generators/Helpers.cs +++ b/src/JsonRpc.Generators/Helpers.cs @@ -35,7 +35,16 @@ public static ExpressionSyntax GetMethodName(InterfaceDeclarationSyntax interfac public static INamedTypeSymbol GetRequestType(INamedTypeSymbol symbol) { var handlerInterface = symbol.AllInterfaces.First(z => z.Name == "IRequestHandler" && z.TypeArguments.Length == 2); - return handlerInterface.TypeArguments[0] as INamedTypeSymbol; + var arg = handlerInterface.TypeArguments[0]; + if (arg is ITypeParameterSymbol typeParameterSymbol) + { + return typeParameterSymbol.ConstraintTypes.OfType().FirstOrDefault(); + } + if (arg is INamedTypeSymbol namedTypeSymbol) + { + return namedTypeSymbol; + } + throw new NotSupportedException("Request Type is not supported!"); } public static INamedTypeSymbol GetResponseType(INamedTypeSymbol symbol) @@ -132,6 +141,31 @@ public static GenericNameSyntax CreateAsyncFunc(ITypeSymbol? responseType, bool .WithTypeArgumentList(TypeArgumentList(SeparatedList(typeArguments))); } + public static GenericNameSyntax CreateDerivedAsyncFunc(ITypeSymbol? responseType, bool withCancellationToken) + { + var typeArguments = new List() { + IdentifierName("T") + }; + if (withCancellationToken) + { + typeArguments.Add(IdentifierName("CancellationToken")); + } + + if (responseType == null || responseType.Name == "Unit") + { + typeArguments.Add(IdentifierName("Task")); + } + else + { + typeArguments.Add(GenericName(Identifier("Task"), TypeArgumentList(SeparatedList(new TypeSyntax[] { + ResolveTypeName(responseType) + })))); + } + + return GenericName(Identifier("Func")) + .WithTypeArgumentList(TypeArgumentList(SeparatedList(typeArguments))); + } + public static GenericNameSyntax CreatePartialAction(ITypeSymbol requestType, NameSyntax partialType, bool withCancellationToken, params ITypeSymbol[] types) { var typeArguments = new List() { @@ -796,7 +830,11 @@ public static string GetExtensionClassName(INamedTypeSymbol symbol) private static string SpecialCasedHandlerFullName(INamedTypeSymbol symbol) { - return new Regex(@"(\w+)$") + if (symbol.IsGenericType) + { + + } + return new Regex(@"(\w+(?:\<\w\>)?)$") .Replace(symbol.ToDisplayString() ?? string.Empty, symbol.Name.Substring(1, symbol.Name.IndexOf("Handler", StringComparison.Ordinal) - 1)) ; diff --git a/src/JsonRpc.Testing/AggregateSettler.cs b/src/JsonRpc.Testing/AggregateSettler.cs index 967c3269f..3bbb6a637 100644 --- a/src/JsonRpc.Testing/AggregateSettler.cs +++ b/src/JsonRpc.Testing/AggregateSettler.cs @@ -6,7 +6,7 @@ namespace OmniSharp.Extensions.JsonRpc.Testing { - class AggregateSettler : ISettler + public class AggregateSettler : ISettler { private readonly ISettler[] _settlers; diff --git a/src/JsonRpc.Testing/JsonRpcTestBase.cs b/src/JsonRpc.Testing/JsonRpcTestBase.cs index f0892237f..852392ee6 100644 --- a/src/JsonRpc.Testing/JsonRpcTestBase.cs +++ b/src/JsonRpc.Testing/JsonRpcTestBase.cs @@ -7,7 +7,7 @@ namespace OmniSharp.Extensions.JsonRpc.Testing { - public abstract class JsonRpcTestBase + public abstract class JsonRpcTestBase : IDisposable { private readonly CancellationTokenSource _cancellationTokenSource; @@ -35,5 +35,11 @@ public JsonRpcTestBase(JsonRpcTestOptions testOptions) protected CancellationToken CancellationToken => _cancellationTokenSource.Token; protected Task SettleNext() => Events.SettleNext(); protected IObservable Settle() => Events.Settle(); + + public void Dispose() + { + _cancellationTokenSource?.Dispose(); + Disposable?.Dispose(); + } } } diff --git a/src/JsonRpc.Testing/SettlePipeline.cs b/src/JsonRpc.Testing/SettlePipeline.cs index 124fbaa2e..6e0b5a4e1 100644 --- a/src/JsonRpc.Testing/SettlePipeline.cs +++ b/src/JsonRpc.Testing/SettlePipeline.cs @@ -4,7 +4,7 @@ namespace OmniSharp.Extensions.JsonRpc.Testing { - class SettlePipeline : IPipelineBehavior + public class SettlePipeline : IPipelineBehavior where T : IRequest { private readonly IRequestSettler _settler; diff --git a/src/JsonRpc.Testing/Settler.cs b/src/JsonRpc.Testing/Settler.cs index 3587bf542..35771b72a 100644 --- a/src/JsonRpc.Testing/Settler.cs +++ b/src/JsonRpc.Testing/Settler.cs @@ -8,7 +8,7 @@ namespace OmniSharp.Extensions.JsonRpc.Testing { - class Settler : ISettler, IRequestSettler + public class Settler : ISettler, IRequestSettler { private readonly TimeSpan _timeout; private readonly CancellationToken _cancellationToken; diff --git a/src/JsonRpc/ContentModified.cs b/src/JsonRpc/ContentModified.cs index edae4abc7..240874bd3 100644 --- a/src/JsonRpc/ContentModified.cs +++ b/src/JsonRpc/ContentModified.cs @@ -5,7 +5,7 @@ namespace OmniSharp.Extensions.JsonRpc { public class ContentModified : RpcError { - internal ContentModified() : base(null, new ErrorMessage(ErrorCodes.ContentModified, "Content Modified")) { } - internal ContentModified(object id ) : base(id, new ErrorMessage(ErrorCodes.ContentModified, "Content Modified")) { } + internal ContentModified(string method) : base(null, method, new ErrorMessage(ErrorCodes.ContentModified, "Content Modified")) { } + internal ContentModified(object id, string method) : base(id, method, new ErrorMessage(ErrorCodes.ContentModified, "Content Modified")) { } } } diff --git a/src/JsonRpc/Generation/GenerateHandlerMethodsAttribute.cs b/src/JsonRpc/Generation/GenerateHandlerMethodsAttribute.cs index 5c909df09..b17ccad1d 100644 --- a/src/JsonRpc/Generation/GenerateHandlerMethodsAttribute.cs +++ b/src/JsonRpc/Generation/GenerateHandlerMethodsAttribute.cs @@ -20,5 +20,10 @@ public GenerateHandlerMethodsAttribute(params Type[] registryTypes) } public string MethodName { get; set; } + + /// + /// Allow the request to be derived and create methods that take a request type argument. + /// + public bool AllowDerivedRequests { get; set; } } } diff --git a/src/JsonRpc/HandlerCollection.cs b/src/JsonRpc/HandlerCollection.cs index a1f85c147..48bafe16e 100644 --- a/src/JsonRpc/HandlerCollection.cs +++ b/src/JsonRpc/HandlerCollection.cs @@ -180,6 +180,16 @@ public IDisposable AddLink(string sourceMethod, string destinationMethod) return h; } + public bool ContainsHandler(Type type) + { + return _handlers.Any(z => type.IsAssignableFrom(z.HandlerType)); + } + + public bool ContainsHandler(TypeInfo type) + { + return _handlers.Any(z => type.IsAssignableFrom(z.HandlerType)); + } + private static readonly Type[] HandlerTypes = { typeof(IJsonRpcNotificationHandler), typeof(IJsonRpcNotificationHandler<>), @@ -190,14 +200,7 @@ public IDisposable AddLink(string sourceMethod, string destinationMethod) private string GetMethodName(Type type) { // Custom method - var attribute = type.GetTypeInfo().GetCustomAttribute(); - if (attribute is null) - { - attribute = type.GetTypeInfo() - .ImplementedInterfaces - .Select(t => t.GetTypeInfo().GetCustomAttribute()) - .FirstOrDefault(x => x != null); - } + var attribute = MethodAttribute.From(type.GetTypeInfo()); // TODO: Log unknown method name if (attribute is null) diff --git a/src/JsonRpc/HandlerTypeDescriptor.cs b/src/JsonRpc/HandlerTypeDescriptor.cs index d186b0b9e..646def496 100644 --- a/src/JsonRpc/HandlerTypeDescriptor.cs +++ b/src/JsonRpc/HandlerTypeDescriptor.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Diagnostics; using System.Linq; using System.Reflection; @@ -11,18 +11,33 @@ class HandlerTypeDescriptor : IHandlerTypeDescriptor { public HandlerTypeDescriptor(Type handlerType) { - var method = handlerType.GetCustomAttribute(); + var method = MethodAttribute.From(handlerType); Method = method.Method; Direction = method.Direction; + if (handlerType.IsGenericTypeDefinition) + { + handlerType = handlerType.MakeGenericType(handlerType.GetTypeInfo().GenericTypeParameters[0].GetGenericParameterConstraints()[0]); + } HandlerType = handlerType; InterfaceType = HandlerTypeDescriptorHelper.GetHandlerInterface(handlerType); - ParamsType = InterfaceType.IsGenericType ? InterfaceType.GetGenericArguments()[0] : typeof(EmptyRequest); - HasParamsType = ParamsType != null && ParamsType != typeof(EmptyRequest); + // This allows for us to have derived types + // We are making the assumption that interface given here + // if a GTD will have a constraint on the first generic type parameter + // that is the real base type for this interface. + if (InterfaceType.IsGenericType) + { + ParamsType = InterfaceType.GetGenericArguments()[0]; + } + else + { + ParamsType = typeof(EmptyRequest); + } + HasParamsType = ParamsType != null && ParamsType != typeof(EmptyRequest); IsNotification = typeof(IJsonRpcNotificationHandler).IsAssignableFrom(handlerType) || handlerType - .GetInterfaces().Any(z => - z.IsGenericType && typeof(IJsonRpcNotificationHandler<>).IsAssignableFrom(z.GetGenericTypeDefinition())); + .GetInterfaces().Any(z => + z.IsGenericType && typeof(IJsonRpcNotificationHandler<>).IsAssignableFrom(z.GetGenericTypeDefinition())); IsRequest = !IsNotification; var requestInterface = ParamsType? diff --git a/src/JsonRpc/HandlerTypeDescriptorHelper.cs b/src/JsonRpc/HandlerTypeDescriptorHelper.cs index aa36a0deb..8fc597d15 100644 --- a/src/JsonRpc/HandlerTypeDescriptorHelper.cs +++ b/src/JsonRpc/HandlerTypeDescriptorHelper.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Concurrent; using System.Collections.Immutable; using System.Linq; @@ -29,10 +29,10 @@ static HandlerTypeDescriptorHelper() } }) .Where(z => z.IsInterface && typeof(IJsonRpcHandler).IsAssignableFrom(z)) - .Where(z => z.GetCustomAttributes().Any()) + .Where(z => MethodAttribute.From(z) != null) .Select(GetMethodType) .Distinct() - .ToLookup(x => x.GetCustomAttribute().Method) + .ToLookup(x => MethodAttribute.From(x).Method) .Select(x => new HandlerTypeDescriptor(x.First()) as IHandlerTypeDescriptor) .ToImmutableSortedDictionary(x => x.Method, x => x, StringComparer.Ordinal); } @@ -82,14 +82,7 @@ public static string GetMethodName(Type type) if (MethodNames.TryGetValue(type, out var method)) return method; // Custom method - var attribute = type.GetCustomAttribute(); - if (attribute is null) - { - attribute = type - .GetInterfaces() - .Select(t => t.GetCustomAttribute()) - .FirstOrDefault(x => x != null); - } + var attribute = MethodAttribute.From(type); var handler = KnownHandlers.Values.FirstOrDefault(z => z.InterfaceType == type || z.HandlerType == type || z.ParamsType == type); @@ -112,14 +105,14 @@ public static string GetMethodName(Type type) internal static Type GetMethodType(Type type) { // Custom method - if (type.GetTypeInfo().GetCustomAttributes().Any()) + if (MethodAttribute.AllFrom(type).Any()) { return type; } return type.GetTypeInfo() .ImplementedInterfaces - .FirstOrDefault(t => t.GetCustomAttributes().Any()); + .FirstOrDefault(t => MethodAttribute.AllFrom(t).Any()); } private static readonly Type[] HandlerTypes = { typeof(IJsonRpcNotificationHandler), typeof(IJsonRpcNotificationHandler<>), typeof(IJsonRpcRequestHandler<>), typeof(IJsonRpcRequestHandler<,>), }; diff --git a/src/JsonRpc/InputHandler.cs b/src/JsonRpc/InputHandler.cs index f4f4077ef..24965c71c 100644 --- a/src/JsonRpc/InputHandler.cs +++ b/src/JsonRpc/InputHandler.cs @@ -18,6 +18,7 @@ using Microsoft.Extensions.Logging; using Nerdbank.Streams; using Newtonsoft.Json; +using OmniSharp.Extensions.JsonRpc.Client; using OmniSharp.Extensions.JsonRpc.Server; using OmniSharp.Extensions.JsonRpc.Server.Messages; using Notification = OmniSharp.Extensions.JsonRpc.Server.Notification; @@ -353,13 +354,13 @@ private void HandleRequest(in ReadOnlySequence request) } catch { - _outputHandler.Send(new ParseError()); + _outputHandler.Send(new ParseError(string.Empty)); return; } if (!_receiver.IsValid(payload)) { - _outputHandler.Send(new InvalidRequest()); + _outputHandler.Send(new InvalidRequest(string.Empty)); return; } @@ -476,9 +477,9 @@ private SchedulerDelegate RouteRequest(IRequestDescriptor de 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)); + return new ErrorResponse(new ContentModified(request.Id, request.Method)); }), - Observable.Timer(_requestTimeout, scheduler).Select(z => new ErrorResponse(new RequestCancelled(request.Id))), + 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); @@ -490,17 +491,17 @@ private SchedulerDelegate RouteRequest(IRequestDescriptor de catch (OperationCanceledException) { _logger.LogTrace("Request {Id} was cancelled", request.Id); - return new RequestCancelled(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, new ErrorMessage(e.Code, e.Message, e.Error)); + 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, e.ToString()); + return new InternalError(request.Id, request.Method, e.ToString()); } }) ) diff --git a/src/JsonRpc/JsonRpc.csproj b/src/JsonRpc/JsonRpc.csproj index 8cb348ece..17688492a 100644 --- a/src/JsonRpc/JsonRpc.csproj +++ b/src/JsonRpc/JsonRpc.csproj @@ -33,5 +33,8 @@ <_Parameter1>OmniSharp.Extensions.LanguageServer.Shared, PublicKey=0024000004800000940000000602000000240000525341310004000001000100391db875e68eb4bfef49ce14313b9e13f2cd3cc89eb273bbe6c11a55044c7d4f566cf092e1c77ef9e7c75b1496ae7f95d925938f5a01793dd8d9f99ae0a7595779b71b971287d7d7b5960d052078d14f5ce1a85ea5c9fb2f59ac735ff7bc215cab469b7c3486006860bad6f4c3b5204ea2f28dd4e1d05e2cca462cfd593b9f9f + + <_Parameter1>OmniSharp.Extensions.DebugAdapter.Shared, PublicKey=0024000004800000940000000602000000240000525341310004000001000100391db875e68eb4bfef49ce14313b9e13f2cd3cc89eb273bbe6c11a55044c7d4f566cf092e1c77ef9e7c75b1496ae7f95d925938f5a01793dd8d9f99ae0a7595779b71b971287d7d7b5960d052078d14f5ce1a85ea5c9fb2f59ac735ff7bc215cab469b7c3486006860bad6f4c3b5204ea2f28dd4e1d05e2cca462cfd593b9f9f + diff --git a/src/JsonRpc/MethodAttribute.cs b/src/JsonRpc/MethodAttribute.cs index d62b607fe..fcbf41007 100644 --- a/src/JsonRpc/MethodAttribute.cs +++ b/src/JsonRpc/MethodAttribute.cs @@ -1,4 +1,7 @@ using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; namespace OmniSharp.Extensions.JsonRpc { @@ -23,5 +26,27 @@ public MethodAttribute(string method, Direction direction) Method = method; Direction = direction; } + + public static MethodAttribute From(Type type) + { + var attribute = type.GetCustomAttribute(true); + if (attribute is null) + { + attribute = type + .GetInterfaces() + .Select(t => t.GetTypeInfo().GetCustomAttribute(true)) + .FirstOrDefault(x => x != null); + } + + return attribute; + } + + public static IEnumerable AllFrom(Type type) + { + return type.GetCustomAttributes(true) + .Concat(type + .GetInterfaces() + .SelectMany(t => t.GetTypeInfo().GetCustomAttributes(true))); + } } } diff --git a/src/JsonRpc/Receiver.cs b/src/JsonRpc/Receiver.cs index f1bae0089..c55e6d695 100644 --- a/src/JsonRpc/Receiver.cs +++ b/src/JsonRpc/Receiver.cs @@ -75,13 +75,13 @@ protected virtual Renor GetRenor(JToken @object) var method = request["method"]?.Value(); if (string.IsNullOrWhiteSpace(method)) { - return new InvalidRequest(requestId, "Method not set"); + return new InvalidRequest(requestId, string.Empty, "Method not set"); } var hasParams = request.TryGetValue("params", out var @params); if (hasParams && @params?.Type != JTokenType.Array && @params?.Type != JTokenType.Object && @params?.Type != JTokenType.Null) { - return new InvalidRequest(requestId, "Invalid params"); + return new InvalidRequest(requestId, method, "Invalid params"); } // Special case params such that if we get a null value (from a non spec compliant system) diff --git a/src/JsonRpc/RequestCancelled.cs b/src/JsonRpc/RequestCancelled.cs index fa6d482f0..5a722e4bd 100644 --- a/src/JsonRpc/RequestCancelled.cs +++ b/src/JsonRpc/RequestCancelled.cs @@ -5,7 +5,7 @@ namespace OmniSharp.Extensions.JsonRpc { public class RequestCancelled : RpcError { - internal RequestCancelled() : base(null, new ErrorMessage(ErrorCodes.RequestCancelled, "Request Cancelled")) { } - internal RequestCancelled(object id) : base(id, new ErrorMessage(ErrorCodes.RequestCancelled, "Request Cancelled")) { } + internal RequestCancelled(string method) : base(null, method, new ErrorMessage(ErrorCodes.RequestCancelled, "Request Cancelled")) { } + internal RequestCancelled(object id, string method) : base(id, method, new ErrorMessage(ErrorCodes.RequestCancelled, "Request Cancelled")) { } } } diff --git a/src/JsonRpc/RequestRouterBase.cs b/src/JsonRpc/RequestRouterBase.cs index af560abed..e5daec356 100644 --- a/src/JsonRpc/RequestRouterBase.cs +++ b/src/JsonRpc/RequestRouterBase.cs @@ -104,7 +104,7 @@ public virtual async Task RouteRequest(IRequestDescriptor(true); + var attribute = MethodAttribute.From(type); if (attribute == null) { throw new NotSupportedException($"Unable to infer method name for type {type.FullName}"); diff --git a/src/JsonRpc/RpcError.cs b/src/JsonRpc/RpcError.cs index 90bdfdf54..abac1496b 100644 --- a/src/JsonRpc/RpcError.cs +++ b/src/JsonRpc/RpcError.cs @@ -10,9 +10,20 @@ public RpcError(object id, ErrorMessage message) { Id = id; Error = message; + Method = string.Empty; + } + + public RpcError(object id, string method, ErrorMessage message) + { + Id = id; + Error = message; + Method = method ?? string.Empty; } public object Id { get; } public ErrorMessage Error { get; } + + [JsonIgnore] + public string Method { get; } } } diff --git a/src/JsonRpc/Server/Messages/InternalError.cs b/src/JsonRpc/Server/Messages/InternalError.cs index fa87ee647..3f8383e6e 100644 --- a/src/JsonRpc/Server/Messages/InternalError.cs +++ b/src/JsonRpc/Server/Messages/InternalError.cs @@ -2,8 +2,8 @@ namespace OmniSharp.Extensions.JsonRpc.Server.Messages { public class InternalError : RpcError { - public InternalError() : this(null) { } - public InternalError(object id) : base(id, new ErrorMessage(ErrorCodes.InternalError, "Internal Error")) { } - public InternalError(object id, string message) : base(id, new ErrorMessage(ErrorCodes.InternalError, "Internal Error - " + message)) { } + public InternalError() : this(null, null) { } + public InternalError(object id, string method) : base(id, method, new ErrorMessage(ErrorCodes.InternalError, "Internal Error")) { } + public InternalError(object id, string method, string message) : base(id, method, new ErrorMessage(ErrorCodes.InternalError, "Internal Error - " + message)) { } } } diff --git a/src/JsonRpc/Server/Messages/InvalidParams.cs b/src/JsonRpc/Server/Messages/InvalidParams.cs index 79f9d1e3d..69b2ac742 100644 --- a/src/JsonRpc/Server/Messages/InvalidParams.cs +++ b/src/JsonRpc/Server/Messages/InvalidParams.cs @@ -2,7 +2,7 @@ namespace OmniSharp.Extensions.JsonRpc.Server.Messages { public class InvalidParams : RpcError { - public InvalidParams() : this(null) { } - public InvalidParams(object id) : base(id, new ErrorMessage(-32602, "Invalid params")) { } + public InvalidParams(string method) : this(null, method) { } + public InvalidParams(object id, string method) : base(id, method, new ErrorMessage(-32602, "Invalid params")) { } } } diff --git a/src/JsonRpc/Server/Messages/InvalidRequest.cs b/src/JsonRpc/Server/Messages/InvalidRequest.cs index ac54cdd8a..5547eef05 100644 --- a/src/JsonRpc/Server/Messages/InvalidRequest.cs +++ b/src/JsonRpc/Server/Messages/InvalidRequest.cs @@ -2,10 +2,8 @@ namespace OmniSharp.Extensions.JsonRpc.Server.Messages { public class InvalidRequest : RpcError { - - public InvalidRequest() : base(null, new ErrorMessage(-32600, $"Invalid Request")) { } - public InvalidRequest(object id) : base(id, new ErrorMessage(-32600, $"Invalid Request")) { } - public InvalidRequest(string message) : base(null, new ErrorMessage(-32600, $"Invalid Request - {message}")) { } - public InvalidRequest(object id, string message) : base(id, new ErrorMessage(-32600, $"Invalid Request - {message}")) { } + public InvalidRequest(string method) : base(null, method, new ErrorMessage(-32600, $"Invalid Request")) { } + public InvalidRequest(string method, string message) : base(null, method, new ErrorMessage(-32600, $"Invalid Request - {message}")) { } + public InvalidRequest(object id, string method, string message) : base(id, method, new ErrorMessage(-32600, $"Invalid Request - {message}")) { } } } diff --git a/src/JsonRpc/Server/Messages/MethodNotFound.cs b/src/JsonRpc/Server/Messages/MethodNotFound.cs index c774a39d4..1902af6bd 100644 --- a/src/JsonRpc/Server/Messages/MethodNotFound.cs +++ b/src/JsonRpc/Server/Messages/MethodNotFound.cs @@ -2,6 +2,6 @@ { public class MethodNotFound : RpcError { - public MethodNotFound(object id, string method) : base(id, new ErrorMessage(-32601, $"Method not found - {method}")) { } + public MethodNotFound(object id, string method) : base(id, method, new ErrorMessage(-32601, $"Method not found - {method}")) { } } } diff --git a/src/JsonRpc/Server/Messages/ParseError.cs b/src/JsonRpc/Server/Messages/ParseError.cs index dfe80e65a..71794d423 100644 --- a/src/JsonRpc/Server/Messages/ParseError.cs +++ b/src/JsonRpc/Server/Messages/ParseError.cs @@ -2,7 +2,7 @@ { public class ParseError : RpcError { - public ParseError() : this(null) { } - public ParseError(object id) : base(id, new ErrorMessage(-32700, "Parse Error")) { } + public ParseError(string method) : this(null, method) { } + public ParseError(object id, string method) : base(id, method, new ErrorMessage(-32700, "Parse Error")) { } } } diff --git a/src/JsonRpc/Server/ServerErrorResult.cs b/src/JsonRpc/Server/ServerErrorResult.cs index 9f8c415d2..f94910cdd 100644 --- a/src/JsonRpc/Server/ServerErrorResult.cs +++ b/src/JsonRpc/Server/ServerErrorResult.cs @@ -16,6 +16,7 @@ public ServerErrorResult(int code, string message) { Code = code; Message = message; + Data = new JObject(); } public int Code { get; set; } @@ -23,4 +24,4 @@ public ServerErrorResult(int code, string message) [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public JToken Data { get; set; } } -} \ No newline at end of file +} diff --git a/src/Protocol/Models/Command.cs b/src/Protocol/Models/Command.cs index af2043877..064e5407f 100644 --- a/src/Protocol/Models/Command.cs +++ b/src/Protocol/Models/Command.cs @@ -31,5 +31,28 @@ public class Command $"{Title}{(string.IsNullOrWhiteSpace(Name) ? "" : $" {Name}")}{(Arguments == null ? "" : string.Join(", ", Arguments.Select(z => z.ToString().Trim('"'))))}"; public override string ToString() => DebuggerDisplay; + + public Command WithCommand(string command) + { + Name = command; + return this; + } + + public Command WithTitle(string title) + { + Title = title; + return this; + } + + public Command WithArguments(params object[] args) + { + Arguments = JArray.FromObject(args); + return this; + } + + public static Command Create(string name, params object[] args) => new Command() { + Name = name, + Arguments = JArray.FromObject(args) + }; } } diff --git a/src/Protocol/Serialization/ContractResolver.cs b/src/Protocol/Serialization/ContractResolver.cs index dad2f64ad..295ebb5dd 100644 --- a/src/Protocol/Serialization/ContractResolver.cs +++ b/src/Protocol/Serialization/ContractResolver.cs @@ -68,7 +68,7 @@ protected override JsonObjectContract CreateObjectContract(Type objectType) protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization) { var property = base.CreateProperty(member, memberSerialization); - if (member.GetCustomAttributes().Any() + if (member.GetCustomAttributes(true).Any() || property.DeclaringType.Name.EndsWith("Capabilities") ) { diff --git a/src/Server/LspServerReceiver.cs b/src/Server/LspServerReceiver.cs index c6fbdba5a..aede9b5ab 100644 --- a/src/Server/LspServerReceiver.cs +++ b/src/Server/LspServerReceiver.cs @@ -29,7 +29,7 @@ public override (IEnumerable results, bool hasResponse) GetRequests(JToke } else if (item.IsRequest) { - newResults.Add(new ServerNotInitialized()); + newResults.Add(new ServerNotInitialized(item.Request.Method)); } else if (item.IsResponse) { diff --git a/src/Server/Messages/ServerNotInitialized.cs b/src/Server/Messages/ServerNotInitialized.cs index 2253044ec..8680c8798 100644 --- a/src/Server/Messages/ServerNotInitialized.cs +++ b/src/Server/Messages/ServerNotInitialized.cs @@ -5,6 +5,6 @@ namespace OmniSharp.Extensions.LanguageServer.Server.Messages { public class ServerNotInitialized : RpcError { - internal ServerNotInitialized() : base(null, new ErrorMessage(-32002, "Server Not Initialized")) { } + internal ServerNotInitialized(string method) : base(null, method, new ErrorMessage(-32002, "Server Not Initialized")) { } } } diff --git a/src/Server/Messages/UnknownErrorCode.cs b/src/Server/Messages/UnknownErrorCode.cs index 2cfc5c396..8e3232d25 100644 --- a/src/Server/Messages/UnknownErrorCode.cs +++ b/src/Server/Messages/UnknownErrorCode.cs @@ -5,6 +5,6 @@ namespace OmniSharp.Extensions.LanguageServer.Server.Messages { public class UnknownErrorCode : RpcError { - internal UnknownErrorCode() : base(null, new ErrorMessage(-32602, "Unknown Error Code")) { } + internal UnknownErrorCode(string method) : base(null, method, new ErrorMessage(-32602, "Unknown Error Code")) { } } } diff --git a/src/Shared/RequestProcessIdentifier.cs b/src/Shared/RequestProcessIdentifier.cs index e4bdbb50d..b6bc40ff4 100644 --- a/src/Shared/RequestProcessIdentifier.cs +++ b/src/Shared/RequestProcessIdentifier.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Concurrent; using System.Linq; using System.Reflection; diff --git a/test/Dap.Tests/DapOutputHandlerTests.cs b/test/Dap.Tests/DapOutputHandlerTests.cs index dbc83a51e..34ac20bc5 100644 --- a/test/Dap.Tests/DapOutputHandlerTests.cs +++ b/test/Dap.Tests/DapOutputHandlerTests.cs @@ -100,7 +100,7 @@ public async Task ShouldSerializeErrors() var received = await reader.ReadToEndAsync(); const string send = - "Content-Length: 76\r\n\r\n{\"seq\":1,\"type\":\"response\",\"request_seq\":1,\"success\":false,\"message\":\"data\"}"; + "Content-Length: 148\r\n\r\n{\"seq\":1,\"type\":\"response\",\"request_seq\":1,\"success\":false,\"command\":\"\",\"message\":\"something\",\"body\":{\"code\":1,\"data\":\"data\",\"message\":\"something\"}}"; received.Should().Be(send); } } diff --git a/test/Dap.Tests/DebugAdapterSpecifictionRecieverTests.cs b/test/Dap.Tests/DebugAdapterSpecifictionRecieverTests.cs index d86611c66..af5ebea36 100644 --- a/test/Dap.Tests/DebugAdapterSpecifictionRecieverTests.cs +++ b/test/Dap.Tests/DebugAdapterSpecifictionRecieverTests.cs @@ -2,6 +2,7 @@ using FluentAssertions; using Newtonsoft.Json; using Newtonsoft.Json.Linq; +using OmniSharp.Extensions.DebugAdapter.Protocol; using OmniSharp.Extensions.JsonRpc; using OmniSharp.Extensions.JsonRpc.Server; using OmniSharp.Extensions.JsonRpc.Server.Messages; @@ -16,6 +17,8 @@ public class DebugAdapterSpecificationReceiverTests public void ShouldRespond_AsExpected(string json, Renor[] request) { var receiver = new DapReceiver(); + var inSerializer = new DapSerializer(); + var outSerializer = new DapSerializer(); var (requests, _) = receiver.GetRequests(JToken.Parse(json)); var result = requests.ToArray(); request.Length.Should().Be(result.Length); @@ -25,8 +28,8 @@ public void ShouldRespond_AsExpected(string json, Renor[] request) var r = request[i]; var response = result[i]; - JsonConvert.SerializeObject(response) - .Should().Be(JsonConvert.SerializeObject(r)); + inSerializer.SerializeObject(response) + .Should().Be(outSerializer.SerializeObject(r)); } } @@ -34,42 +37,42 @@ class SpecificationMessages : TheoryData { public SpecificationMessages() { - Add ( + Add( @"{""seq"": ""0"", ""type"": ""request"", ""command"": ""attach"", ""arguments"": { ""__restart"": 3 }}", new Renor[] { new Request(0, "attach", new JObject() {{"__restart", 3}}) } ); - Add ( + Add( @"{""seq"": ""1"", ""type"": ""request"", ""command"": ""attach""}", new Renor[] { new Request(1, "attach", new JObject()) } ); - Add ( + Add( @"{""seq"": ""0"", ""type"": ""event"", ""event"": ""breakpoint"", ""body"": { ""reason"": ""new"" }}", new Renor[] { new Notification("breakpoint", new JObject() {{"reason", "new"}}), } ); - Add ( + Add( @"{""seq"": ""1"", ""type"": ""event"", ""event"": ""breakpoint""}", new Renor[] { new Notification("breakpoint", null) } ); - Add ( + Add( @"{""seq"": ""1"", ""type"": ""response"", ""request_seq"": 3, ""success"": true, ""command"": ""attach"", ""body"": { }}", new Renor[] { new ServerResponse(3, new JObject()), } ); - Add ( + Add( @"{""seq"": ""1"", ""type"": ""response"", ""request_seq"": 3, ""success"": true, ""command"": ""attach"", ""body"": null}", new Renor[] { new ServerResponse(3, null), @@ -84,17 +87,17 @@ public SpecificationMessages() // } // ); - Add ( + Add( @"{""seq"": ""1"", ""type"": ""response"", ""request_seq"": 3, ""success"": false, ""command"": ""attach"", ""body"": null}", new Renor[] { - new ServerError(3, null), + new ServerError(3, new ServerErrorResult(-1, "Unknown Error", new JObject())), } ); - Add ( + Add( @"[1]", new Renor[] { - new InvalidRequest("Not an object") + new InvalidRequest(string.Empty, "Not an object") }); } } diff --git a/test/Dap.Tests/FoundationTests.cs b/test/Dap.Tests/FoundationTests.cs index fc5a1df19..9bcd62c06 100644 --- a/test/Dap.Tests/FoundationTests.cs +++ b/test/Dap.Tests/FoundationTests.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Reflection; @@ -9,6 +9,7 @@ using MediatR; using Microsoft.Extensions.Logging; using NSubstitute; +using OmniSharp.Extensions.DebugAdapter.Protocol.Events; using OmniSharp.Extensions.DebugAdapter.Protocol.Requests; using OmniSharp.Extensions.JsonRpc; using Xunit; @@ -29,7 +30,7 @@ public FoundationTests(ITestOutputHelper outputHelper) [ClassData(typeof(ParamsShouldHaveMethodAttributeData))] public void ParamsShouldHaveMethodAttribute(Type type) { - type.GetCustomAttributes().Any(z => z.Direction != Direction.Unspecified).Should() + MethodAttribute.AllFrom(type).Any(z => z.Direction != Direction.Unspecified).Should() .Be(true, $"{type.Name} is missing a method attribute or the direction is not specified"); } @@ -37,7 +38,7 @@ public void ParamsShouldHaveMethodAttribute(Type type) [ClassData(typeof(HandlersShouldHaveMethodAttributeData))] public void HandlersShouldHaveMethodAttribute(Type type) { - type.GetCustomAttributes().Any(z => z.Direction != Direction.Unspecified).Should() + MethodAttribute.AllFrom(type).Any(z => z.Direction != Direction.Unspecified).Should() .Be(true, $"{type.Name} is missing a method attribute or the direction is not specified"); } @@ -48,8 +49,8 @@ public void HandlersShouldMatchParamsMethodAttribute(Type type) if (typeof(IJsonRpcNotificationHandler).IsAssignableFrom(type)) return; var paramsType = HandlerTypeDescriptorHelper.GetHandlerInterface(type).GetGenericArguments()[0]; - var lhs = type.GetCustomAttribute(true); - var rhs = paramsType.GetCustomAttribute(true); + var lhs = MethodAttribute.From(type); + var rhs = MethodAttribute.From(paramsType); lhs.Method.Should().Be(rhs.Method, $"{type.FullName} method does not match {paramsType.FullName}"); lhs.Direction.Should().Be(rhs.Direction, $"{type.FullName} direction does not match {paramsType.FullName}"); } @@ -293,8 +294,9 @@ public class HandlersShouldHaveMethodAttributeData : TheoryData public HandlersShouldHaveMethodAttributeData() { foreach (var type in typeof(IDataBreakpointInfoHandler).Assembly.ExportedTypes - .Where(z => z.IsInterface && typeof(IJsonRpcHandler).IsAssignableFrom(z) && !z.IsGenericType)) + .Where(z => z.IsInterface && typeof(IJsonRpcHandler).IsAssignableFrom(z))) { + if (type.IsGenericTypeDefinition && !MethodAttribute.AllFrom(type).Any()) continue; Add(type); } } @@ -305,8 +307,9 @@ public class HandlersShouldAbstractClassData : TheoryData public HandlersShouldAbstractClassData() { foreach (var type in typeof(IDataBreakpointInfoHandler).Assembly.ExportedTypes - .Where(z => z.IsInterface && typeof(IJsonRpcHandler).IsAssignableFrom(z) && !z.IsGenericType)) + .Where(z => z.IsInterface && typeof(IJsonRpcHandler).IsAssignableFrom(z))) { + if (type.IsGenericTypeDefinition && !MethodAttribute.AllFrom(type).Any()) continue; Add(type); } } @@ -342,8 +345,11 @@ public class TypeHandlerData : TheoryData public TypeHandlerData() { foreach (var type in typeof(CompletionsArguments).Assembly.ExportedTypes.Where( - z => z.IsInterface && typeof(IJsonRpcHandler).IsAssignableFrom(z) && !z.IsGenericType)) + z => z.IsInterface && typeof(IJsonRpcHandler).IsAssignableFrom(z))) { + if (type.IsGenericTypeDefinition && !MethodAttribute.AllFrom(type).Any()) continue; + if (type == typeof(IProgressStartHandler) || type == typeof(IProgressUpdateHandler) || type == typeof(IProgressEndHandler)) continue; + Add(HandlerTypeDescriptorHelper.GetHandlerTypeDescriptor(type)); } } @@ -354,8 +360,10 @@ public class TypeHandlerExtensionData : TheoryData z.IsInterface && typeof(IJsonRpcHandler).IsAssignableFrom(z) && !z.IsGenericType)) + .Where(z => z.IsInterface && typeof(IJsonRpcHandler).IsAssignableFrom(z))) { + if (type.IsGenericTypeDefinition && !MethodAttribute.AllFrom(type).Any()) continue; + if (type == typeof(IProgressStartHandler) || type == typeof(IProgressUpdateHandler) || type == typeof(IProgressEndHandler)) continue; var descriptor = HandlerTypeDescriptorHelper.GetHandlerTypeDescriptor(type); Add( @@ -371,42 +379,23 @@ public TypeHandlerExtensionData() private static string GetExtensionClassName(IHandlerTypeDescriptor descriptor) { - return SpecialCasedHandlerFullName(descriptor) + "Extensions"; + return SpecialCasedHandlerName(descriptor) + "Extensions"; ; } - private static string SpecialCasedHandlerFullName(IHandlerTypeDescriptor descriptor) + private static string SpecialCasedHandlerName(IHandlerTypeDescriptor descriptor) { - return new Regex(@"(\w+)$") - .Replace(descriptor.HandlerType.FullName ?? string.Empty, + return new Regex(@"(\w+(?:\`\d)?)$") + .Replace(descriptor.HandlerType.Name ?? string.Empty, descriptor.HandlerType.Name.Substring(1, descriptor.HandlerType.Name.IndexOf("Handler", StringComparison.Ordinal) - 1)) ; } - private static string HandlerName(IHandlerTypeDescriptor descriptor) - { - var name = HandlerFullName(descriptor); - return name.Substring(name.LastIndexOf('.') + 1); - } - - private static string HandlerFullName(IHandlerTypeDescriptor descriptor) - { - return new Regex(@"(\w+)$") - .Replace(descriptor.HandlerType.FullName ?? string.Empty, - descriptor.HandlerType.Name.Substring(1, descriptor.HandlerType.Name.IndexOf("Handler", StringComparison.Ordinal) - 1)); - } - - private static string SpecialCasedHandlerName(IHandlerTypeDescriptor descriptor) - { - var name = SpecialCasedHandlerFullName(descriptor); - return name.Substring(name.LastIndexOf('.') + 1); - } - private static Type GetExtensionClass(IHandlerTypeDescriptor descriptor) { var name = GetExtensionClassName(descriptor); return descriptor.HandlerType.Assembly.GetExportedTypes() - .FirstOrDefault(z => z.IsClass && z.FullName == name); + .FirstOrDefault(z => z.IsClass && z.IsAbstract && (z.Name == name || z.Name == name + "Base")); } private static string GetOnMethodName(IHandlerTypeDescriptor descriptor) diff --git a/test/Dap.Tests/Integration/ConnectionAndDisconnectionTests.cs b/test/Dap.Tests/Integration/ConnectionAndDisconnectionTests.cs new file mode 100644 index 000000000..7ec756049 --- /dev/null +++ b/test/Dap.Tests/Integration/ConnectionAndDisconnectionTests.cs @@ -0,0 +1,106 @@ +using System; +using System.Threading.Tasks; +using FluentAssertions; +using NSubstitute; +using OmniSharp.Extensions.DebugAdapter.Client; +using OmniSharp.Extensions.DebugAdapter.Server; +using OmniSharp.Extensions.DebugAdapter.Testing; +using OmniSharp.Extensions.JsonRpc.Server; +using OmniSharp.Extensions.JsonRpc.Testing; +using Xunit; +using Xunit.Abstractions; + +namespace Dap.Tests.Integration +{ + public class ConnectionAndDisconnectionTests : DebugAdapterProtocolTestBase + { + public ConnectionAndDisconnectionTests(ITestOutputHelper outputHelper) : base(new JsonRpcTestOptions() + .ConfigureForXUnit(outputHelper) + .WithTestTimeout(TimeSpan.FromSeconds(20)) + ) + { + } + + [Fact] + public async Task Server_Should_Stay_Alive_When_Requests_Throw_An_Exception() + { + var (client, server) = await Initialize(ConfigureClient, ConfigureServer); + + var result = await client.SendRequest("keepalive").Returning(CancellationToken); + result.Should().BeTrue(); + + Func a = () => client.SendRequest("throw").ReturningVoid(CancellationToken); + await a.Should().ThrowAsync(); + + result = await client.SendRequest("keepalive").Returning(CancellationToken); + result.Should().BeTrue(); + } + + [Fact] + public async Task Client_Should_Stay_Alive_When_Requests_Throw_An_Exception() + { + var (client, server) = await Initialize(ConfigureClient, ConfigureServer); + + var result = await server.SendRequest("keepalive").Returning(CancellationToken); + result.Should().BeTrue(); + + Func a = () => server.SendRequest("throw").ReturningVoid(CancellationToken); + await a.Should().ThrowAsync(); + + result = await server.SendRequest("keepalive").Returning(CancellationToken); + result.Should().BeTrue(); + } + + [Fact] + public async Task Server_Should_Support_Links() + { + var (client, server) = await Initialize(ConfigureClient, ConfigureServer); + + var result = await client.SendRequest("ka").Returning(CancellationToken); + result.Should().BeTrue(); + + Func a = () => client.SendRequest("t").ReturningVoid(CancellationToken); + await a.Should().ThrowAsync(); + + result = await client.SendRequest("ka").Returning(CancellationToken); + result.Should().BeTrue(); + } + + [Fact] + public async Task Client_Should_Support_Links() + { + var (client, server) = await Initialize(ConfigureClient, ConfigureServer); + + var result = await server.SendRequest("ka").Returning(CancellationToken); + result.Should().BeTrue(); + + Func a = () => server.SendRequest("t").ReturningVoid(CancellationToken); + await a.Should().ThrowAsync(); + + result = await server.SendRequest("ka").Returning(CancellationToken); + result.Should().BeTrue(); + } + + private void ConfigureClient(DebugAdapterClientOptions options) + { + options.OnRequest("keepalive", (ct) => Task.FromResult(true)); + options.WithLink("keepalive", "ka"); + options.WithLink("throw", "t"); + options.OnRequest("throw", async ct => { + throw new NotSupportedException(); + return Task.CompletedTask; + }); + } + + private void ConfigureServer(DebugAdapterServerOptions options) + { + options.OnRequest("keepalive", (ct) => Task.FromResult(true)); + options.WithLink("keepalive", "ka"); + options.WithLink("throw", "t"); + options.OnRequest("throw", async ct => { + throw new NotSupportedException(); + return Task.CompletedTask; + }); + } + } +} diff --git a/test/Dap.Tests/Integration/CustomRequestsTests.cs b/test/Dap.Tests/Integration/CustomRequestsTests.cs new file mode 100644 index 000000000..85b4f6786 --- /dev/null +++ b/test/Dap.Tests/Integration/CustomRequestsTests.cs @@ -0,0 +1,267 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using FluentAssertions; +using NSubstitute; +using OmniSharp.Extensions.DebugAdapter.Protocol.Requests; +using OmniSharp.Extensions.DebugAdapter.Testing; +using OmniSharp.Extensions.JsonRpc; +using OmniSharp.Extensions.JsonRpc.Testing; +using Xunit; +using Xunit.Abstractions; + +namespace Dap.Tests.Integration +{ + public class CustomRequestsTests : DebugAdapterProtocolTestBase + { + public CustomRequestsTests(ITestOutputHelper outputHelper) : base(new JsonRpcTestOptions() + .ConfigureForXUnit(outputHelper) + .WithSettleTimeSpan(TimeSpan.FromMilliseconds(200)) + ) + { + } + + [Fact] + public async Task Should_Support_Custom_Attach_Request_Using_Base_Class() + { + var fake = Substitute.For>(); + var (client, server) = await Initialize(options => { }, options => { options.AddHandler(fake); }); + + await client.RequestAttach(new CustomAttachRequestArguments() { + ComputerName = "computer", + RunspaceId = "1234", + ProcessId = "4321" + }); + + var call = fake.ReceivedCalls().Single(); + var args = call.GetArguments(); + var request = args[0].Should().BeOfType().Which; + request.ComputerName.Should().Be("computer"); + request.RunspaceId.Should().Be("1234"); + request.ProcessId.Should().Be("4321"); + } + + [Fact] + public async Task Should_Support_Custom_Attach_Request_Receiving_Regular_Request_Using_Base_Class() + { + var fake = Substitute.For(); + var (client, server) = await Initialize(options => { }, options => { options.AddHandler(fake); }); + + await client.RequestAttach(new CustomAttachRequestArguments() { + ComputerName = "computer", + RunspaceId = "1234", + ProcessId = "4321" + }); + + var call = fake.ReceivedCalls().Single(); + var args = call.GetArguments(); + var request = args[0].Should().BeOfType().Which; + request.ExtensionData.Should().ContainKey("ComputerName").And.Subject["ComputerName"].Should().Be("computer"); + request.ExtensionData.Should().ContainKey("RunspaceId").And.Subject["RunspaceId"].Should().Be("1234"); + request.ExtensionData.Should().ContainKey("ProcessId").And.Subject["ProcessId"].Should().Be("4321"); + } + + [Fact] + public async Task Should_Support_Custom_Attach_Request_Using_Extension_Data_Using_Base_Class() + { + var fake = Substitute.For>(); + var (client, server) = await Initialize(options => { }, options => { options.AddHandler(fake); }); + + await client.RequestAttach(new AttachRequestArguments() { + ExtensionData = new Dictionary() { + ["ComputerName"] = "computer", + ["RunspaceId"] = "1234", + ["ProcessId"] = "4321" + } + }); + + var call = fake.ReceivedCalls().Single(); + var args = call.GetArguments(); + var request = args[0].Should().BeOfType().Which; + request.ComputerName.Should().Be("computer"); + request.RunspaceId.Should().Be("1234"); + request.ProcessId.Should().Be("4321"); + } + + [Fact] + public async Task Should_Support_Custom_Launch_Request_Using_Base_Class() + { + var fake = Substitute.For>(); + var (client, server) = await Initialize(options => { }, options => { options.AddHandler(fake); }); + + await client.RequestLaunch(new CustomLaunchRequestArguments() { + Script = "build.ps1" + }); + + var call = fake.ReceivedCalls().Single(); + var args = call.GetArguments(); + var request = args[0].Should().BeOfType().Which; + request.Script.Should().Be("build.ps1"); + } + + [Fact] + public async Task Should_Support_Custom_Launch_Request_Receiving_Regular_Request_Using_Base_Class() + { + var fake = Substitute.For(); + var (client, server) = await Initialize(options => { }, options => { options.AddHandler(fake); }); + + await client.RequestLaunch(new CustomLaunchRequestArguments() { + Script = "build.ps1" + }); + + var call = fake.ReceivedCalls().Single(); + var args = call.GetArguments(); + var request = args[0].Should().BeOfType().Which; + request.ExtensionData.Should().ContainKey("Script").And.Subject["Script"].Should().Be("build.ps1"); + } + + [Fact] + public async Task Should_Support_Custom_Launch_Request_Using_Extension_Data_Base_Class() + { + var fake = Substitute.For>(); + var (client, server) = await Initialize(options => { }, options => { options.AddHandler(fake); }); + + await client.RequestLaunch(new CustomLaunchRequestArguments() { + ExtensionData = new Dictionary() { + ["Script"] = "build.ps1" + } + }); + + var call = fake.ReceivedCalls().Single(); + var args = call.GetArguments(); + var request = args[0].Should().BeOfType().Which; + request.Script.Should().Be("build.ps1"); + } + + [Fact] + public async Task Should_Support_Custom_Attach_Request_Using_Delegate() + { + var fake = Substitute.For>>(); + var (client, server) = await Initialize(options => { }, options => { options.OnAttach(fake); }); + + await client.RequestAttach(new CustomAttachRequestArguments() { + ComputerName = "computer", + RunspaceId = "1234", + ProcessId = "4321" + }); + + var call = fake.ReceivedCalls().Single(); + var args = call.GetArguments(); + var request = args[0].Should().BeOfType().Which; + request.ComputerName.Should().Be("computer"); + request.RunspaceId.Should().Be("1234"); + request.ProcessId.Should().Be("4321"); + } + + [Fact] + public async Task Should_Support_Custom_Attach_Request_Receiving_Regular_Request_Using_Delegate() + { + var fake = Substitute.For>>(); + var (client, server) = await Initialize(options => { }, options => { options.OnAttach(fake); }); + + await client.RequestAttach(new CustomAttachRequestArguments() { + ComputerName = "computer", + RunspaceId = "1234", + ProcessId = "4321" + }); + + var call = fake.ReceivedCalls().Single(); + var args = call.GetArguments(); + var request = args[0].Should().BeOfType().Which; + request.ExtensionData.Should().ContainKey("ComputerName").And.Subject["ComputerName"].Should().Be("computer"); + request.ExtensionData.Should().ContainKey("RunspaceId").And.Subject["RunspaceId"].Should().Be("1234"); + request.ExtensionData.Should().ContainKey("ProcessId").And.Subject["ProcessId"].Should().Be("4321"); + } + + [Fact] + public async Task Should_Support_Custom_Attach_Request_Using_Extension_Data_Using_Delegate() + { + var fake = Substitute.For>>(); + var (client, server) = await Initialize(options => { }, options => { options.OnAttach(fake); }); + + await client.RequestAttach(new AttachRequestArguments() { + ExtensionData = new Dictionary() { + ["ComputerName"] = "computer", + ["RunspaceId"] = "1234", + ["ProcessId"] = "4321" + } + }); + + var call = fake.ReceivedCalls().Single(); + var args = call.GetArguments(); + var request = args[0].Should().BeOfType().Which; + request.ComputerName.Should().Be("computer"); + request.RunspaceId.Should().Be("1234"); + request.ProcessId.Should().Be("4321"); + } + + [Fact] + public async Task Should_Support_Custom_Launch_Request_Using_Delegate() + { + var fake = Substitute.For>>(); + var (client, server) = await Initialize(options => { }, options => { options.OnLaunch(fake); }); + + await client.RequestLaunch(new CustomLaunchRequestArguments() { + Script = "build.ps1" + }); + + var call = fake.ReceivedCalls().Single(); + var args = call.GetArguments(); + var request = args[0].Should().BeOfType().Which; + request.Script.Should().Be("build.ps1"); + } + + [Fact] + public async Task Should_Support_Custom_Launch_Request_Receiving_Regular_Request_Using_Delegate() + { + var fake = Substitute.For>>(); + var (client, server) = await Initialize(options => { }, options => { options.OnLaunch(fake); }); + + await client.RequestLaunch(new CustomLaunchRequestArguments() { + Script = "build.ps1" + }); + + var call = fake.ReceivedCalls().Single(); + var args = call.GetArguments(); + var request = args[0].Should().BeOfType().Which; + request.ExtensionData.Should().ContainKey("Script").And.Subject["Script"].Should().Be("build.ps1"); + } + + [Fact] + public async Task Should_Support_Custom_Launch_Request_Using_Extension_Data_Using_Delegate() + { + var fake = Substitute.For>>(); + var (client, server) = await Initialize(options => { }, options => { options.OnLaunch(fake); }); + + await client.RequestLaunch(new CustomLaunchRequestArguments() { + ExtensionData = new Dictionary() { + ["Script"] = "build.ps1" + } + }); + + var call = fake.ReceivedCalls().Single(); + var args = call.GetArguments(); + var request = args[0].Should().BeOfType().Which; + request.Script.Should().Be("build.ps1"); + } + + public class CustomAttachRequestArguments : AttachRequestArguments + { + public string ComputerName { get; set; } + + public string ProcessId { get; set; } + + public string RunspaceId { get; set; } + } + + public class CustomLaunchRequestArguments : LaunchRequestArguments + { + /// + /// Gets or sets the absolute path to the script to debug. + /// + public string Script { get; set; } + } + } +} diff --git a/test/Dap.Tests/Integration/GenericDapServerTests.cs b/test/Dap.Tests/Integration/GenericDapServerTests.cs new file mode 100644 index 000000000..a8951067e --- /dev/null +++ b/test/Dap.Tests/Integration/GenericDapServerTests.cs @@ -0,0 +1,83 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using FluentAssertions; +using NSubstitute; +using OmniSharp.Extensions.DebugAdapter.Protocol.Requests; +using OmniSharp.Extensions.DebugAdapter.Testing; +using OmniSharp.Extensions.JsonRpc.Testing; +using Xunit; +using Xunit.Abstractions; + +namespace Dap.Tests.Integration +{ + public class GenericDapServerTests : DebugAdapterProtocolTestBase + { + public GenericDapServerTests(ITestOutputHelper outputHelper) : base(new JsonRpcTestOptions() + .ConfigureForXUnit(outputHelper) + .WithSettleTimeSpan(TimeSpan.FromMilliseconds(200)) + ) + { + } + + [Fact] + public async Task Supports_Multiple_Handlers_On_A_Single_Class() + { + var handler = new Handler(); + var (client, server) = await Initialize(options => { }, options => { options.AddHandler(handler); }); + + server.ServerSettings.SupportsStepBack.Should().Be(true); + server.ServerSettings.SupportsStepInTargetsRequest.Should().Be(true); + + await client.RequestStepBack(new StepBackArguments()); + await client.RequestReverseContinue(new ReverseContinueArguments()); + await client.RequestStepInTargets(new StepInTargetsArguments()); + await client.RequestStepOut(new StepOutArguments()); + await client.RequestStepIn(new StepInArguments()); + await client.RequestNext(new NextArguments()); + + handler.Count.Should().Be(6); + } + + class Handler : IStepBackHandler, IStepInTargetsHandler, IStepInHandler, IStepOutHandler, INextHandler, IReverseContinueHandler + { + public int Count { get; set; } = 0; + + public Task Handle(StepBackArguments request, CancellationToken cancellationToken) + { + Count++; + return Task.FromResult(new StepBackResponse()); + } + + public Task Handle(StepInTargetsArguments request, CancellationToken cancellationToken) + { + Count++; + return Task.FromResult(new StepInTargetsResponse()); + } + + public Task Handle(StepInArguments request, CancellationToken cancellationToken) + { + Count++; + return Task.FromResult(new StepInResponse()); + } + + public Task Handle(StepOutArguments request, CancellationToken cancellationToken) + { + Count++; + return Task.FromResult(new StepOutResponse()); + } + + public Task Handle(NextArguments request, CancellationToken cancellationToken) + { + Count++; + return Task.FromResult(new NextResponse()); + } + + public Task Handle(ReverseContinueArguments request, CancellationToken cancellationToken) + { + Count++; + return Task.FromResult(new ReverseContinueResponse()); + } + } + } +} diff --git a/test/Dap.Tests/Integration/ProgressTests.cs b/test/Dap.Tests/Integration/ProgressTests.cs new file mode 100644 index 000000000..802b6c5a1 --- /dev/null +++ b/test/Dap.Tests/Integration/ProgressTests.cs @@ -0,0 +1,197 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reactive.Linq; +using System.Threading.Tasks; +using FluentAssertions; +using NSubstitute; +using OmniSharp.Extensions.DebugAdapter.Client; +using OmniSharp.Extensions.DebugAdapter.Protocol.Events; +using OmniSharp.Extensions.DebugAdapter.Protocol.Models; +using OmniSharp.Extensions.DebugAdapter.Server; +using OmniSharp.Extensions.DebugAdapter.Testing; +using OmniSharp.Extensions.JsonRpc.Testing; +using Xunit; +using Xunit.Abstractions; + +namespace Dap.Tests.Integration +{ + public class ProgressTests : DebugAdapterProtocolTestBase + { + public ProgressTests(ITestOutputHelper outputHelper) : base(new JsonRpcTestOptions() + .ConfigureForXUnit(outputHelper) + .WithSettleTimeSpan(TimeSpan.FromMilliseconds(200)) + ) + { + } + + class Data + { + public string Value { get; set; } = "Value"; + } + + [Fact] + public async Task Should_Support_Progress_From_Sever_To_Client() + { + var (client, server) = await Initialize(ConfigureClient, ConfigureServer); + + var data = new List(); + client.ProgressManager.Progress.Take(1).Switch().Subscribe(x => data.Add(x)); + + using var workDoneObserver = server.ProgressManager.Create(new ProgressStartEvent() { + Cancellable = true, + Message = "Begin", + Percentage = 0, + Title = "Work is pending" + }, onComplete: () => new ProgressEndEvent() { + Message = "End" + }); + + workDoneObserver.OnNext(new ProgressUpdateEvent() { + Percentage = 10, + Message = "Report 1" + }); + + workDoneObserver.OnNext(new ProgressUpdateEvent() { + Percentage = 20, + Message = "Report 2" + }); + + workDoneObserver.OnNext(new ProgressUpdateEvent() { + Percentage = 30, + Message = "Report 3" + }); + + workDoneObserver.OnNext(new ProgressUpdateEvent() { + Percentage = 40, + Message = "Report 4" + }); + + workDoneObserver.OnCompleted(); + + await SettleNext(); + + var results = data.Select(z => z switch { + ProgressStartEvent begin => begin.Message, + ProgressUpdateEvent begin => begin.Message, + ProgressEndEvent begin => begin.Message, + }); + + results.Should().ContainInOrder("Begin", "Report 1", "Report 2", "Report 3", "Report 4", "End"); + } + + [Fact] + public async Task Should_Support_Observing_Progress_From_Client_To_Server_Request() + { + var (client, server) = await Initialize(ConfigureClient, ConfigureServer); + + var data = new List(); + client.ProgressManager.Progress.Take(1).Switch().Subscribe(x => data.Add(x)); + + using var workDoneObserver = server.ProgressManager.Create(new ProgressStartEvent() { + Cancellable = true, + Message = "Begin", + Percentage = 0, + Title = "Work is pending" + }, onComplete: () => new ProgressEndEvent() { + Message = "End" + }); + + workDoneObserver.OnNext(new ProgressUpdateEvent() { + Percentage = 10, + Message = "Report 1" + }); + + workDoneObserver.OnNext(new ProgressUpdateEvent() { + Percentage = 20, + Message = "Report 2" + }); + + workDoneObserver.OnNext(new ProgressUpdateEvent() { + Percentage = 30, + Message = "Report 3" + }); + + workDoneObserver.OnNext(new ProgressUpdateEvent() { + Percentage = 40, + Message = "Report 4" + }); + + workDoneObserver.OnCompleted(); + + await SettleNext(); + + var results = data.Select(z => z switch { + ProgressStartEvent begin => begin.Message, + ProgressUpdateEvent begin => begin.Message, + ProgressEndEvent begin => begin.Message, + }); + + results.Should().ContainInOrder("Begin", "Report 1", "Report 2", "Report 3", "Report 4", "End"); + } + + [Fact] + public async Task Should_Support_Cancelling_Progress_From_Client_To_Server_Request() + { + var (client, server) = await Initialize(ConfigureClient, ConfigureServer); + + var data = new List(); + var sub = client.ProgressManager.Progress.Take(1).Switch().Subscribe(x => data.Add(x)); + + using var workDoneObserver = server.ProgressManager.Create(new ProgressStartEvent() { + Cancellable = true, + Message = "Begin", + Percentage = 0, + Title = "Work is pending" + }, onComplete: () => new ProgressEndEvent() { + Message = "End" + }); + + workDoneObserver.OnNext(new ProgressUpdateEvent() { + Percentage = 10, + Message = "Report 1" + }); + + workDoneObserver.OnNext(new ProgressUpdateEvent() { + Percentage = 20, + Message = "Report 2" + }); + + await SettleNext(); + sub.Dispose(); + await SettleNext(); + + workDoneObserver.OnNext(new ProgressUpdateEvent() { + Percentage = 30, + Message = "Report 3" + }); + + workDoneObserver.OnNext(new ProgressUpdateEvent() { + Percentage = 40, + Message = "Report 4" + }); + + workDoneObserver.OnCompleted(); + + await SettleNext(); + + var results = data.Select(z => z switch { + ProgressStartEvent begin => begin.Message, + ProgressUpdateEvent begin => begin.Message, + ProgressEndEvent begin => begin.Message, + }); + + results.Should().ContainInOrder("Begin", "Report 1", "Report 2"); + } + + private void ConfigureClient(DebugAdapterClientOptions options) + { + + } + + private void ConfigureServer(DebugAdapterServerOptions options) + { + // options.OnCodeLens() + } + } +} diff --git a/test/Dap.Tests/Integration/RequestCancellationTests.cs b/test/Dap.Tests/Integration/RequestCancellationTests.cs new file mode 100644 index 000000000..e7659c519 --- /dev/null +++ b/test/Dap.Tests/Integration/RequestCancellationTests.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using FluentAssertions; +using Microsoft.CodeAnalysis; +using Microsoft.Extensions.Logging; +using NSubstitute; +using OmniSharp.Extensions.DebugAdapter.Client; +using OmniSharp.Extensions.DebugAdapter.Protocol.Models; +using OmniSharp.Extensions.DebugAdapter.Protocol.Requests; +using OmniSharp.Extensions.DebugAdapter.Server; +using OmniSharp.Extensions.DebugAdapter.Testing; +using OmniSharp.Extensions.JsonRpc.Server; +using OmniSharp.Extensions.JsonRpc.Testing; +using Xunit; +using Xunit.Abstractions; + +namespace Dap.Tests.Integration +{ + + public class RequestCancellationTests : DebugAdapterProtocolTestBase + { + public RequestCancellationTests(ITestOutputHelper outputHelper) : base(new JsonRpcTestOptions() + .ConfigureForXUnit(outputHelper) + .WithSettleTimeSpan(TimeSpan.FromMilliseconds(200)) + ) + { + } + + [Fact] + public async Task Should_Cancel_Pending_Requests() + { + var (client, server) = await Initialize(ConfigureClient, ConfigureServer); + + Func> action = () => { + var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(100)); + CancellationToken.Register(cts.Cancel); + return client.RequestCompletions(new CompletionsArguments(), cts.Token); + }; + action.Should().Throw(); + } + + [Fact] + public async Task Should_Cancel_Requests_After_Timeout() + { + var (client, server) = await Initialize(ConfigureClient, x => { + ConfigureServer(x); + x.WithMaximumRequestTimeout(TimeSpan.FromMilliseconds(500)); + }); + + Func> action = () => client.RequestCompletions(new CompletionsArguments()); + action.Should().Throw(); + } + + private void ConfigureClient(DebugAdapterClientOptions options) + { + } + + private void ConfigureServer(DebugAdapterServerOptions options) + { + options.OnCompletions(async (x, ct) => { + await Task.Delay(50000, ct); + return new CompletionsResponse(); + }); + } + } +} diff --git a/test/Generation.Tests/JsonRpcGenerationTests.cs b/test/Generation.Tests/JsonRpcGenerationTests.cs index a33d4deef..2e5501a51 100644 --- a/test/Generation.Tests/JsonRpcGenerationTests.cs +++ b/test/Generation.Tests/JsonRpcGenerationTests.cs @@ -1,4 +1,4 @@ -using System.Threading.Tasks; +using System.Threading.Tasks; using Snapper; using Snapper.Attributes; using Snapper.Core; @@ -548,7 +548,8 @@ public static partial class DefinitionExtensions } [Fact] - public async Task Supports_Custom_Method_Names() { + public async Task Supports_Custom_Method_Names() + { var source = @" using System; using System.Threading; @@ -595,6 +596,116 @@ public static partial class LanguageProtocolInitializeExtensions { public static Task RequestLanguageProtocolInitialize(this ITextDocumentLanguageClient mediator, InitializeParams @params, CancellationToken cancellationToken = default) => mediator.SendRequest(@params, cancellationToken); } +}"; + await AssertGeneratedAsExpected(source, expected); + } + + [Fact] + public async Task Supports_Allow_Derived_Requests() + { + var source = @" +using System; +using System.Threading; +using System.Threading.Tasks; +using OmniSharp.Extensions.JsonRpc; +using OmniSharp.Extensions.JsonRpc.Generation; +using System.Collections.Generic; +using MediatR; + +namespace OmniSharp.Extensions.DebugAdapter.Protocol.Requests +{ + [Parallel, Method(RequestNames.Attach, Direction.ClientToServer)] + [GenerateHandlerMethods(AllowDerivedRequests = true), GenerateRequestMethods] + public interface IAttachHandler : IJsonRpcRequestHandler { } +}"; + var expected = @" +using System; +using System.Threading; +using System.Threading.Tasks; +using OmniSharp.Extensions.JsonRpc; +using OmniSharp.Extensions.JsonRpc.Generation; +using System.Collections.Generic; +using MediatR; +using Microsoft.Extensions.DependencyInjection; +using OmniSharp.Extensions.DebugAdapter.Protocol; + +namespace OmniSharp.Extensions.DebugAdapter.Protocol.Requests +{ + [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverageAttribute, System.Runtime.CompilerServices.CompilerGeneratedAttribute] + public static partial class AttachExtensions + { + public static IDebugAdapterServerRegistry OnAttach(this IDebugAdapterServerRegistry registry, Func> handler) => registry.AddHandler(RequestNames.Attach, RequestHandler.For(handler)); + public static IDebugAdapterServerRegistry OnAttach(this IDebugAdapterServerRegistry registry, Func> handler) => registry.AddHandler(RequestNames.Attach, RequestHandler.For(handler)); + public static IDebugAdapterServerRegistry OnAttach(this IDebugAdapterServerRegistry registry, Func> handler) + where T : AttachRequestArguments => registry.AddHandler(RequestNames.Attach, RequestHandler.For(handler)); + public static IDebugAdapterServerRegistry OnAttach(this IDebugAdapterServerRegistry registry, Func> handler) + where T : AttachRequestArguments => registry.AddHandler(RequestNames.Attach, RequestHandler.For(handler)); + } +} + +namespace OmniSharp.Extensions.DebugAdapter.Protocol.Requests +{ + public static partial class AttachExtensions + { + public static Task RequestAttach(this IDebugAdapterClient mediator, AttachRequestArguments @params, CancellationToken cancellationToken = default) => mediator.SendRequest(@params, cancellationToken); + } +}"; + await AssertGeneratedAsExpected(source, expected); + } + [Fact] + public async Task Supports_Allow_Generic_Types() + { + var source = @" +using System; +using System.Threading; +using System.Threading.Tasks; +using OmniSharp.Extensions.JsonRpc; +using OmniSharp.Extensions.JsonRpc.Generation; +using System.Collections.Generic; +using MediatR; + +namespace OmniSharp.Extensions.DebugAdapter.Protocol.Bogus +{ + public class AttachResponse { } + [Method(""attach"", Direction.ClientToServer)] + public class AttachRequestArguments: IRequest { } + + [Parallel, Method(""attach"", Direction.ClientToServer)] + [GenerateHandlerMethods(AllowDerivedRequests = true), GenerateRequestMethods] + public interface IAttachHandler : IJsonRpcRequestHandler where T : AttachRequestArguments { } + public interface IAttachHandler : IAttachHandler { } +}"; + var expected = @" +using System; +using System.Threading; +using System.Threading.Tasks; +using OmniSharp.Extensions.JsonRpc; +using OmniSharp.Extensions.JsonRpc.Generation; +using System.Collections.Generic; +using MediatR; +using Microsoft.Extensions.DependencyInjection; +using OmniSharp.Extensions.DebugAdapter.Protocol; + +namespace OmniSharp.Extensions.DebugAdapter.Protocol.Bogus +{ + [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverageAttribute, System.Runtime.CompilerServices.CompilerGeneratedAttribute] + public static partial class AttachExtensions + { + public static IDebugAdapterServerRegistry OnAttach(this IDebugAdapterServerRegistry registry, Func> handler) => registry.AddHandler(""attach"", RequestHandler.For(handler)); + public static IDebugAdapterServerRegistry OnAttach(this IDebugAdapterServerRegistry registry, Func> handler) => registry.AddHandler(""attach"", RequestHandler.For(handler)); + public static IDebugAdapterServerRegistry OnAttach(this IDebugAdapterServerRegistry registry, Func> handler) + where T : AttachRequestArguments => registry.AddHandler(""attach"", RequestHandler.For(handler)); + public static IDebugAdapterServerRegistry OnAttach(this IDebugAdapterServerRegistry registry, Func> handler) + where T : AttachRequestArguments => registry.AddHandler(""attach"", RequestHandler.For(handler)); + } +} + +namespace OmniSharp.Extensions.DebugAdapter.Protocol.Bogus +{ + public static partial class AttachExtensions + { + public static Task RequestAttach(this IDebugAdapterClient mediator, AttachRequestArguments @params, CancellationToken cancellationToken = default) => mediator.SendRequest(@params, cancellationToken); + } }"; await AssertGeneratedAsExpected(source, expected); } diff --git a/test/JsonRpc.Tests/Server/SpecifictionRecieverTests.cs b/test/JsonRpc.Tests/Server/SpecifictionRecieverTests.cs index 306566733..a6f5e2af6 100644 --- a/test/JsonRpc.Tests/Server/SpecifictionRecieverTests.cs +++ b/test/JsonRpc.Tests/Server/SpecifictionRecieverTests.cs @@ -110,23 +110,23 @@ public SpecificationMessages() @"{""jsonrpc"": ""2.0"", ""method"": 1, ""params"": ""bar""}", new Renor[] { - new InvalidRequest("Invalid params") + new InvalidRequest("1", "Invalid params") }); Add ( @"[1]", new Renor[] { - new InvalidRequest("Not an object") + new InvalidRequest("", "Not an object") }); Add ( @"[1,2,3]", new Renor[] { - new InvalidRequest("Not an object"), - new InvalidRequest("Not an object"), - new InvalidRequest("Not an object") + new InvalidRequest("", "Not an object"), + new InvalidRequest("", "Not an object"), + new InvalidRequest("", "Not an object") }); Add ( @@ -143,16 +143,16 @@ public SpecificationMessages() new Request("1", "sum", new JArray(new [] {1,2,4})), new Notification("notify_hello", new JArray(new [] {7})), new Request("2", "subtract", new JArray(new [] {42,23})), - new InvalidRequest("Unexpected protocol"), + new InvalidRequest("", "Unexpected protocol"), new Request("5", "foo.get", JObject.FromObject(new {name = "myself"})), new Request("9", "get_data", null), }); Add ( @"[ - {""jsonrpc"": ""2.0"", ""error"": {""code"": -32600, ""message"": ""Invalid Request""}, ""id"": null}, - {""jsonrpc"": ""2.0"", ""error"": {""code"": -32600, ""message"": ""Invalid Request""}, ""id"": null}, - {""jsonrpc"": ""2.0"", ""error"": {""code"": -32600, ""message"": ""Invalid Request""}, ""id"": null} + {""jsonrpc"": ""2.0"", ""error"": {""code"": -32600, ""message"": ""Invalid Request"", ""data"": {}}, ""id"": null}, + {""jsonrpc"": ""2.0"", ""error"": {""code"": -32600, ""message"": ""Invalid Request"", ""data"": {}}, ""id"": null}, + {""jsonrpc"": ""2.0"", ""error"": {""code"": -32600, ""message"": ""Invalid Request"", ""data"": {}}, ""id"": null} ]", new Renor[] { new ServerError(new ServerErrorResult(-32600, "Invalid Request")), diff --git a/test/Lsp.Tests/FoundationTests.cs b/test/Lsp.Tests/FoundationTests.cs index 2bb4f6517..757cd6034 100644 --- a/test/Lsp.Tests/FoundationTests.cs +++ b/test/Lsp.Tests/FoundationTests.cs @@ -66,7 +66,7 @@ public DebuggerDisplayTypes() [ClassData(typeof(ParamsShouldHaveMethodAttributeData))] public void ParamsShouldHaveMethodAttribute(Type type) { - type.GetCustomAttributes().Any(z => z.Direction != Direction.Unspecified).Should() + MethodAttribute.AllFrom(type).Any(z => z.Direction != Direction.Unspecified).Should() .Be(true, $"{type.Name} is missing a method attribute or the direction is not specified"); } @@ -74,7 +74,7 @@ public void ParamsShouldHaveMethodAttribute(Type type) [ClassData(typeof(HandlersShouldHaveMethodAttributeData))] public void HandlersShouldHaveMethodAttribute(Type type) { - type.GetCustomAttributes().Any(z => z.Direction != Direction.Unspecified).Should() + MethodAttribute.AllFrom(type).Any(z => z.Direction != Direction.Unspecified).Should() .Be(true, $"{type.Name} is missing a method attribute or the direction is not specified"); } @@ -85,8 +85,8 @@ public void HandlersShouldMatchParamsMethodAttribute(Type type) if (typeof(IJsonRpcNotificationHandler).IsAssignableFrom(type)) return; var paramsType = HandlerTypeDescriptorHelper.GetHandlerInterface(type).GetGenericArguments()[0]; - var lhs = type.GetCustomAttribute(true); - var rhs = paramsType.GetCustomAttribute(true); + var lhs = MethodAttribute.From(type); + var rhs = MethodAttribute.From(paramsType); lhs.Method.Should().Be(rhs.Method, $"{type.FullName} method does not match {paramsType.FullName}"); lhs.Direction.Should().Be(rhs.Direction, $"{type.FullName} direction does not match {paramsType.FullName}"); } @@ -474,9 +474,10 @@ public class HandlersShouldHaveMethodAttributeData : TheoryData { public HandlersShouldHaveMethodAttributeData() { - foreach (var type in typeof(CompletionParams).Assembly.ExportedTypes.Where(z => z.IsInterface && typeof(IJsonRpcHandler).IsAssignableFrom(z) && !z.IsGenericType) + foreach (var type in typeof(CompletionParams).Assembly.ExportedTypes.Where(z => z.IsInterface && typeof(IJsonRpcHandler).IsAssignableFrom(z)) .Except(new[] {typeof(ITextDocumentSyncHandler)})) { + if (type.IsGenericTypeDefinition && !MethodAttribute.AllFrom(type).Any()) continue; Add(type); } } @@ -486,9 +487,10 @@ public class TypeHandlerData : TheoryData { public TypeHandlerData() { - foreach (var type in typeof(CompletionParams).Assembly.ExportedTypes.Where(z => z.IsInterface && typeof(IJsonRpcHandler).IsAssignableFrom(z) && !z.IsGenericType) + foreach (var type in typeof(CompletionParams).Assembly.ExportedTypes.Where(z => z.IsInterface && typeof(IJsonRpcHandler).IsAssignableFrom(z)) .Except(new[] {typeof(ITextDocumentSyncHandler)})) { + if (type.IsGenericTypeDefinition && !MethodAttribute.AllFrom(type).Any()) continue; Add(LspHandlerTypeDescriptorHelper.GetHandlerTypeDescriptor(type)); } } @@ -505,12 +507,19 @@ public class TypeHandlerExtensionData : TheoryData z.IsInterface && typeof(IJsonRpcHandler).IsAssignableFrom(z) && !z.IsGenericType) + .Where(z => z.IsInterface && typeof(IJsonRpcHandler).IsAssignableFrom(z)) .Except(new[] {typeof(ITextDocumentSyncHandler)})) { + if (type.IsGenericTypeDefinition && !MethodAttribute.AllFrom(type).Any()) continue; if (type == typeof(ICompletionResolveHandler) || type == typeof(ICodeLensResolveHandler) || type == typeof(IDocumentLinkResolveHandler)) continue; + if (type == typeof(ISemanticTokensHandler) || type == typeof(ISemanticTokensDeltaHandler) || type == typeof(ISemanticTokensRangeHandler)) continue; var descriptor = LspHandlerTypeDescriptorHelper.GetHandlerTypeDescriptor(type); + if (descriptor == null) + { + throw new Exception(""); + } + Add( descriptor, GetOnMethodName(descriptor), @@ -522,50 +531,28 @@ public TypeHandlerExtensionData() } } - private static string GetExtensionClassName(ILspHandlerTypeDescriptor descriptor) + private static string GetExtensionClassName(IHandlerTypeDescriptor descriptor) { - return SpecialCasedHandlerFullName(descriptor) + "Extensions"; + return SpecialCasedHandlerName(descriptor) + "Extensions"; ; } - private static string SpecialCasedHandlerFullName(ILspHandlerTypeDescriptor descriptor) + private static string SpecialCasedHandlerName(IHandlerTypeDescriptor descriptor) { - return new Regex(@"(\w+)$") - .Replace(descriptor.HandlerType.FullName ?? string.Empty, + return new Regex(@"(\w+(?:\`\d)?)$") + .Replace(descriptor.HandlerType.Name ?? string.Empty, descriptor.HandlerType.Name.Substring(1, descriptor.HandlerType.Name.IndexOf("Handler", StringComparison.Ordinal) - 1)) - .Replace("SemanticTokensEdits", "SemanticTokens") - .Replace("SemanticTokensDelta", "SemanticTokens") - .Replace("SemanticTokensRange", "SemanticTokens") ; } - private static string HandlerName(ILspHandlerTypeDescriptor descriptor) - { - var name = HandlerFullName(descriptor); - return name.Substring(name.LastIndexOf('.') + 1); - } - - private static string HandlerFullName(ILspHandlerTypeDescriptor descriptor) - { - return new Regex(@"(\w+)$") - .Replace(descriptor.HandlerType.FullName ?? string.Empty, - descriptor.HandlerType.Name.Substring(1, descriptor.HandlerType.Name.IndexOf("Handler", StringComparison.Ordinal) - 1)); - } - - public static string SpecialCasedHandlerName(ILspHandlerTypeDescriptor descriptor) - { - var name = SpecialCasedHandlerFullName(descriptor); - return name.Substring(name.LastIndexOf('.') + 1); - } - - private static Type GetExtensionClass(ILspHandlerTypeDescriptor descriptor) + private static Type GetExtensionClass(IHandlerTypeDescriptor descriptor) { var name = GetExtensionClassName(descriptor); return descriptor.HandlerType.Assembly.GetExportedTypes() - .FirstOrDefault(z => z.IsClass && z.FullName == name); + .FirstOrDefault(z => z.IsClass && z.IsAbstract && (z.Name == name || z.Name == name + "Base")); } - private static string GetOnMethodName(ILspHandlerTypeDescriptor descriptor) + private static string GetOnMethodName(IHandlerTypeDescriptor descriptor) { return "On" + SpecialCasedHandlerName(descriptor); } diff --git a/test/Lsp.Tests/Integration/ExecuteCommandTests.cs b/test/Lsp.Tests/Integration/ExecuteCommandTests.cs index ec82648dd..5898debda 100644 --- a/test/Lsp.Tests/Integration/ExecuteCommandTests.cs +++ b/test/Lsp.Tests/Integration/ExecuteCommandTests.cs @@ -35,10 +35,7 @@ public async Task Should_Execute_A_Command() options => { }, options => { options.OnCompletion(x => { return Task.FromResult(new CompletionList(new CompletionItem() { - Command = new Command() { - Name = "execute-a", - Arguments = JArray.FromObject(new object[] { 1, "2", false }) - } + Command = Command.Create("execute-a", 1, "2", false) })); }, new CompletionRegistrationOptions() { }); @@ -72,10 +69,7 @@ public async Task Should_Execute_The_Correct_Command() }, options => { options.OnCompletion(x => { return Task.FromResult(new CompletionList(new CompletionItem() { - Command = new Command() { - Name = "execute-b", - Arguments = JArray.FromObject(new object[] { 1, "2", false }) - } + Command = Command.Create("execute-b", 1, "2", false ) })); }, new CompletionRegistrationOptions() { }); @@ -110,10 +104,7 @@ public async Task Should_Fail_To_Execute_A_Command_When_No_Command_Is_Defined() options => { }, options => { options.OnCompletion(x => { return Task.FromResult(new CompletionList(new CompletionItem() { - Command = new Command() { - Name = "execute-a", - Arguments = JArray.FromObject(new object[] { 1, "2", false }) - } + Command = Command.Create("execute-a", 1, "2", false ) })); }, new CompletionRegistrationOptions() { }); @@ -170,10 +161,7 @@ public async Task Should_Fail_To_Execute_A_Command() options => { }, options => { options.OnCompletion(x => { return Task.FromResult(new CompletionList(new CompletionItem() { - Command = new Command() { - Name = "execute-a", - Arguments = JArray.FromObject(new object[] { 1, "2", false }) - } + Command = Command.Create("execute-a",1, "2", false ) })); }, new CompletionRegistrationOptions() { }); @@ -207,10 +195,7 @@ public async Task Should_Execute_1_Args() options => { }, options => { options.OnCompletion(x => { return Task.FromResult(new CompletionList(new CompletionItem() { - Command = new Command() { - Name = "execute-a", - Arguments = JArray.FromObject(new object[] { 1 }) - } + Command = Command.Create("execute-a", 1 ) })); }, new CompletionRegistrationOptions() { }); @@ -239,10 +224,7 @@ public async Task Should_Execute_2_Args() options => { }, options => { options.OnCompletion(x => { return Task.FromResult(new CompletionList(new CompletionItem() { - Command = new Command() { - Name = "execute-a", - Arguments = JArray.FromObject(new object[] { 1, "2" }) - } + Command = Command.Create("execute-a", 1, "2" ) })); }, new CompletionRegistrationOptions() { }); @@ -272,10 +254,7 @@ public async Task Should_Execute_3_Args() options => { }, options => { options.OnCompletion(x => { return Task.FromResult(new CompletionList(new CompletionItem() { - Command = new Command() { - Name = "execute-a", - Arguments = JArray.FromObject(new object[] { 1, "2", true }) - } + Command = Command.Create("execute-a", 1, "2", true ) })); }, new CompletionRegistrationOptions() { }); @@ -306,10 +285,7 @@ public async Task Should_Execute_4_Args() options => { }, options => { options.OnCompletion(x => { return Task.FromResult(new CompletionList(new CompletionItem() { - Command = new Command() { - Name = "execute-a", - Arguments = JArray.FromObject(new object[] { 1, "2", true, new Range((0, 1), (1, 1)) }) - } + Command = Command.Create("execute-a", 1, "2", true, new Range((0, 1), (1, 1)) ) })); }, new CompletionRegistrationOptions() { }); @@ -341,10 +317,7 @@ public async Task Should_Execute_5_Args() options => { }, options => { options.OnCompletion(x => { return Task.FromResult(new CompletionList(new CompletionItem() { - Command = new Command() { - Name = "execute-a", - Arguments = JArray.FromObject(new object[] { 1, "2", true, new Range((0, 1), (1, 1)), new Dictionary() { ["a"] = "123", ["b"] = "456" } }) - } + Command = Command.Create("execute-a", 1, "2", true, new Range((0, 1), (1, 1)), new Dictionary() { ["a"] = "123", ["b"] = "456" } ) })); }, new CompletionRegistrationOptions() { }); @@ -377,10 +350,7 @@ public async Task Should_Execute_6_Args() options => { }, options => { options.OnCompletion(x => { return Task.FromResult(new CompletionList(new CompletionItem() { - Command = new Command() { - Name = "execute-a", - Arguments = JArray.FromObject(new object[] { 1, "2", true, new Range((0, 1), (1, 1)), new Dictionary() { ["a"] = "123", ["b"] = "456" }, Guid.NewGuid() }) - } + Command = Command.Create("execute-a",1, "2", true, new Range((0, 1), (1, 1)), new Dictionary() { ["a"] = "123", ["b"] = "456" }, Guid.NewGuid() ) })); }, new CompletionRegistrationOptions() { }); @@ -414,10 +384,7 @@ public async Task Should_Execute_1_With_Missing_Args() options => { }, options => { options.OnCompletion(x => { return Task.FromResult(new CompletionList(new CompletionItem() { - Command = new Command() { - Name = "execute-a", - Arguments = JArray.FromObject(new object[] { }) - } + Command = Command.Create("execute-a") })); }, new CompletionRegistrationOptions() { }); @@ -446,10 +413,7 @@ public async Task Should_Execute_2_With_Missing_Args() options => { }, options => { options.OnCompletion(x => { return Task.FromResult(new CompletionList(new CompletionItem() { - Command = new Command() { - Name = "execute-a", - Arguments = JArray.FromObject(new object[] { }) - } + Command = Command.Create("execute-a") })); }, new CompletionRegistrationOptions() { }); @@ -479,10 +443,7 @@ public async Task Should_Execute_3_With_Missing_Args() options => { }, options => { options.OnCompletion(x => { return Task.FromResult(new CompletionList(new CompletionItem() { - Command = new Command() { - Name = "execute-a", - Arguments = JArray.FromObject(new object[] { }) - } + Command = Command.Create("execute-a") })); }, new CompletionRegistrationOptions() { }); @@ -513,10 +474,7 @@ public async Task Should_Execute_4_With_Missing_Args() options => { }, options => { options.OnCompletion(x => { return Task.FromResult(new CompletionList(new CompletionItem() { - Command = new Command() { - Name = "execute-a", - Arguments = JArray.FromObject(new object[] { }) - } + Command = Command.Create("execute-a") })); }, new CompletionRegistrationOptions() { }); @@ -548,10 +506,7 @@ public async Task Should_Execute_5_With_Missing_Args() options => { }, options => { options.OnCompletion(x => { return Task.FromResult(new CompletionList(new CompletionItem() { - Command = new Command() { - Name = "execute-a", - Arguments = JArray.FromObject(new object[] { }) - } + Command = Command.Create("execute-a") })); }, new CompletionRegistrationOptions() { }); @@ -584,10 +539,7 @@ public async Task Should_Execute_6_With_Missing_Args() options => { }, options => { options.OnCompletion(x => { return Task.FromResult(new CompletionList(new CompletionItem() { - Command = new Command() { - Name = "execute-a", - Arguments = JArray.FromObject(new object[] { }) - } + Command = Command.Create("execute-a") })); }, new CompletionRegistrationOptions() { }); @@ -621,9 +573,7 @@ public async Task Should_Execute_1_Null_Args() options => { }, options => { options.OnCompletion(x => { return Task.FromResult(new CompletionList(new CompletionItem() { - Command = new Command() { - Name = "execute-a" - } + Command = Command.Create("execute-a") })); }, new CompletionRegistrationOptions() { }); @@ -652,9 +602,7 @@ public async Task Should_Execute_2_Null_Args() options => { }, options => { options.OnCompletion(x => { return Task.FromResult(new CompletionList(new CompletionItem() { - Command = new Command() { - Name = "execute-a" - } + Command = Command.Create("execute-a") })); }, new CompletionRegistrationOptions() { }); @@ -684,9 +632,7 @@ public async Task Should_Execute_3_Null_Args() options => { }, options => { options.OnCompletion(x => { return Task.FromResult(new CompletionList(new CompletionItem() { - Command = new Command() { - Name = "execute-a" - } + Command = Command.Create("execute-a") })); }, new CompletionRegistrationOptions() { }); @@ -717,9 +663,7 @@ public async Task Should_Execute_4_Null_Args() options => { }, options => { options.OnCompletion(x => { return Task.FromResult(new CompletionList(new CompletionItem() { - Command = new Command() { - Name = "execute-a" - } + Command = Command.Create("execute-a") })); }, new CompletionRegistrationOptions() { }); @@ -751,9 +695,7 @@ public async Task Should_Execute_5_Null_Args() options => { }, options => { options.OnCompletion(x => { return Task.FromResult(new CompletionList(new CompletionItem() { - Command = new Command() { - Name = "execute-a" - } + Command = Command.Create("execute-a") })); }, new CompletionRegistrationOptions() { }); @@ -786,9 +728,7 @@ public async Task Should_Execute_6_Null_Args() options => { }, options => { options.OnCompletion(x => { return Task.FromResult(new CompletionList(new CompletionItem() { - Command = new Command() { - Name = "execute-a" - } + Command = Command.Create("execute-a") })); }, new CompletionRegistrationOptions() { }); diff --git a/test/Lsp.Tests/Integration/RequestCancellationTests.cs b/test/Lsp.Tests/Integration/RequestCancellationTests.cs index 92fe147ee..7ec05c61d 100644 --- a/test/Lsp.Tests/Integration/RequestCancellationTests.cs +++ b/test/Lsp.Tests/Integration/RequestCancellationTests.cs @@ -33,11 +33,13 @@ public async Task Should_Cancel_Pending_Requests() { var (client, server) = await Initialize(ConfigureClient, ConfigureServer); - var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(10)); - CancellationToken.Register(cts.Cancel); - Func> action = () => client.TextDocument.RequestCompletion(new CompletionParams() { - TextDocument = "/a/file.cs" - }, cts.Token).AsTask(); + Func> action = () => { + var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(10)); + CancellationToken.Register(cts.Cancel); + return client.TextDocument.RequestCompletion(new CompletionParams() { + TextDocument = "/a/file.cs" + }, cts.Token).AsTask(); + }; action.Should().Throw(); } diff --git a/test/Lsp.Tests/Messages/ServerNotInitializedTests.cs b/test/Lsp.Tests/Messages/ServerNotInitializedTests.cs index c72f90e1f..61a6eb536 100644 --- a/test/Lsp.Tests/Messages/ServerNotInitializedTests.cs +++ b/test/Lsp.Tests/Messages/ServerNotInitializedTests.cs @@ -12,7 +12,7 @@ public class ServerNotInitializedTests [Theory, JsonFixture] public void SimpleTest(string expected) { - var model = new ServerNotInitialized(); + var model = new ServerNotInitialized(""); var result = Fixture.SerializeObject(model); result.Should().Be(expected); diff --git a/test/Lsp.Tests/Messages/UnknownErrorCodeTests.cs b/test/Lsp.Tests/Messages/UnknownErrorCodeTests.cs index 24b7c0722..ddd9beb6e 100644 --- a/test/Lsp.Tests/Messages/UnknownErrorCodeTests.cs +++ b/test/Lsp.Tests/Messages/UnknownErrorCodeTests.cs @@ -12,7 +12,7 @@ public class UnknownErrorCodeTests [Theory, JsonFixture] public void SimpleTest(string expected) { - var model = new UnknownErrorCode(); + var model = new UnknownErrorCode(""); var result = Fixture.SerializeObject(model); result.Should().Be(expected);