diff --git a/Directory.Packages.props b/Directory.Packages.props index 8d08c3338..acfeb9c94 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,6 +1,8 @@ + netstandard2.1;netstandard2.0;net8.0;net10.0 + net10.0 $(BaseIntermediateOutputPath)\GeneratedFiles diff --git a/architecture.md b/architecture.md new file mode 100644 index 000000000..7addf37b0 --- /dev/null +++ b/architecture.md @@ -0,0 +1,242 @@ +# Architecture: Language Server Protocol implementation + +This repository implements the Language Server Protocol (LSP) as a set of layered .NET packages. The lower layers provide a bidirectional JSON-RPC runtime; the LSP layers add protocol models, serializers, request routing, client/server startup, capability negotiation, and dynamic registration. + +## Package layout + +| Area | Project | Role | +| --- | --- | --- | +| JSON-RPC runtime | `src\JsonRpc` | Transport framing, request/response dispatch, handler registration, request scheduling, cancellation, serialization primitives, and DI integration. | +| LSP protocol model | `src\Protocol` | LSP request/notification models, client and server capabilities, registration options, typed protocol proxy interfaces, serializers, converters, and handler base classes. | +| Shared LSP runtime | `src\Shared` | LSP-specific handler collection, descriptor creation, descriptor matching, routing, supported capability tracking, progress support, and common DI registrations. | +| Server facade | `src\Server` | `LanguageServer`, server lifecycle, initialization handling, server capability calculation, dynamic registration, configuration, workspace folders, and server-side proxies. | +| Client facade | `src\Client` | `LanguageClient`, client lifecycle, initialize request creation, client capability construction, static/dynamic registration tracking, and client-side proxies. | +| Source generators | `src\JsonRpc.Generators` | Compile-time generation for handler interfaces, handler base classes, delegate registration methods, protocol proxy request methods, container helpers, and enum-like strings. | +| Proposals | `src\Protocol.Proposals` | Protocol additions that are not yet stable in the base LSP package. | +| Testing support | `src\Testing` and `src\JsonRpc.Testing` | In-memory protocol test harnesses and request settling utilities. | + +The Debug Adapter Protocol projects follow the same broad architecture, but this document focuses on LSP. + +## Layering model + +The implementation is intentionally split so the JSON-RPC runtime is protocol-agnostic: + +```text +Streams / PipeReader / PipeWriter + | + v +src\JsonRpc + Connection -> InputHandler / OutputHandler + Receiver -> RequestRouter -> RequestInvoker -> handlers + ResponseRouter <- outgoing requests + | + v +src\Protocol + src\Shared + LSP models, LspSerializer, LspRequestRouter, + LspHandlerDescriptor, capabilities, registration options + | + v +src\Server / src\Client + LanguageServer and LanguageClient lifecycle, + initialize handshake, facades, work done/progress, + dynamic registration and configuration helpers +``` + +LSP features are represented as typed request or notification records in `src\Protocol\Features`. Those records carry protocol metadata through attributes such as `[Method]`, `[Parallel]`, `[Serial]`, `[RegistrationOptions]`, `[Capability]`, and `[Resolver]`. Source generators consume that metadata to produce the public handler and proxy APIs. + +For example, `CompletionParams` in `src\Protocol\Features\Document\CompletionFeature.cs` is a request model for `textDocument/completion`. Its attributes define the JSON-RPC method name, direction, request scheduling behavior, registration option type, client capability type, and generated APIs. The generator turns that model into handler interfaces/base classes and convenience methods on language client/server protocol proxies. + +## JSON-RPC transport and dispatch + +`src\JsonRpc` handles the wire-level JSON-RPC mechanics used by LSP: + +1. `Connection` owns an `InputHandler` and opens the input loop. +2. `InputHandler` reads LSP-style `Content-Length: ...\r\n\r\n` framed messages from a `PipeReader`, parses the JSON payload with Newtonsoft.Json, validates it through an `IReceiver`, and separates requests, notifications, responses, and errors. +3. Incoming responses are completed through `IResponseRouter`, which maps response IDs back to pending outgoing requests. +4. Incoming requests and notifications are routed through `IRequestRouter`. +5. `RequestInvoker` invokes the selected handler descriptors, applies serial/parallel scheduling, request timeouts, cancellation, and error mapping. +6. `OutputHandler` serializes outgoing messages, writes JSON-RPC framing to a `PipeWriter`, and can delay messages until output filters allow them. + +The JSON-RPC layer knows only about JSON-RPC handlers and method names. It does not know about document selectors, LSP capabilities, dynamic registration, or LSP-specific union types; those are added by the LSP shared layer. + +## LSP models and serialization + +`src\Protocol` maps LSP specification concepts into .NET types: + +- Request and notification parameter records implement MediatR request shapes such as `IRequest` for requests and `IRequest` for notifications. +- Protocol models live mostly under `src\Protocol\Models` and feature files under `src\Protocol\Features`. +- Client capabilities are modeled under `src\Protocol\Client\Capabilities`. +- Server capabilities are modeled under `src\Protocol\Server\Capabilities`. +- Registration options and capability interfaces describe how features participate in static and dynamic registration. +- Helper types such as `Container`, `BooleanOr`, `DocumentUri`, `Position`, and `Range` smooth over TypeScript-oriented LSP shapes in idiomatic C#. + +`LspSerializer` in `src\Protocol\Serialization\Serializer.cs` extends the JSON-RPC serializer with LSP-specific converters and contract resolution. It handles protocol union shapes, optional values, string/number enums, `DocumentUri`, progress tokens, completion lists, markup content, semantic token shapes, workspace edits, and other LSP-specific wire formats. + +The serializer is capability-aware. During initialization, the client and server feed negotiated capabilities into the serializer so it can constrain serialized enum-like values to what the peer supports, such as completion item kinds, symbol kinds, diagnostic tags, code action kinds, and semantic token types. + +## Handler model + +Handlers are normal .NET objects implementing generated or generic JSON-RPC interfaces: + +- `IJsonRpcNotificationHandler` +- `IJsonRpcRequestHandler` +- `IJsonRpcRequestHandler` +- generated LSP-specific interfaces such as completion, hover, or initialize handlers +- abstract base classes in `AbstractHandlers` for handlers that need registration options, capabilities, partial results, or initial values + +Handlers can be registered as instances, types, factories, or delegates. The registry APIs are exposed by options objects at construction time and by runtime `Register(...)` methods on `LanguageServer`, `LanguageClient`, and the underlying JSON-RPC server. + +`SharedHandlerCollection` is the central LSP handler registry. When a handler is added, it: + +- inspects the handler's implemented interfaces, +- asks `LspHandlerTypeDescriptorProvider` for method/capability/registration metadata, +- creates `LspHandlerDescriptor` entries, +- tracks text document identifiers where applicable, +- infers request process type from explicit options or `[Serial]` / `[Parallel]`, +- assigns stable routing keys after initialization based on registration options, document selectors, execute-command names, and handler IDs. + +After the collection is initialized, descriptors contain enough information for both request routing and capability registration. + +## LSP request routing + +`LspRequestRouter` in `src\Shared` specializes JSON-RPC routing for LSP. It first finds descriptors by JSON-RPC method name, deserializes the incoming params to the descriptor's parameter type, and then applies registered `IHandlerMatcher` strategies. + +This enables LSP-specific routing behavior: + +- text document requests can be routed by `DocumentSelector`, +- execute-command requests can be routed by command name, +- resolve requests can be sent back to the handler that created the original item, +- multiple handlers for one method can be selected when the protocol feature supports aggregation. + +If no LSP matcher selects a descriptor, the router falls back to all descriptors for the method. Execute-command is stricter: if no command-specific handler matches, the route is treated as missing. + +## Client and server lifecycle + +Both `LanguageServer` and `LanguageClient` derive from `JsonRpcServerBase`, because JSON-RPC is bidirectional: either peer can send requests and receive requests. + +### Server startup + +`LanguageServer` in `src\Server\LanguageServer.cs` is created through `LanguageServer.Create(...)`, `LanguageServer.From(...)`, or DI registration. Its container is built from `JsonRpcServerContainer` plus LSP/server internals. + +At initialization: + +1. The server opens the JSON-RPC connection and waits for `initialize`. +2. The generated internal initialize handler receives raw client capability JSON. +3. The server deserializes client capabilities, records the client LSP version, initializes `SupportedCapabilities`, and updates `LspSerializer`. +4. Server initialize callbacks run. +5. The handler collection is initialized, producing final descriptor keys and registration options. +6. Server capabilities are computed from registered handlers and registration-option converters. +7. Initialized callbacks run. +8. The receiver is marked initialized so normal incoming messages can be processed. +9. For LSP 3 clients, the server completes startup after receiving `initialized`. +10. Started callbacks run and the server is marked started. + +Static capabilities are returned in the `InitializeResult`. Dynamic registrations are delayed until initialization is complete. + +### Client startup + +`LanguageClient` in `src\Client\LanguageClient.cs` follows the complementary flow: + +1. It builds `InitializeParams` from client options, root URI, workspace folders, initialization options, trace settings, and client capabilities. +2. Capability instances are merged into `ClientCapabilities`. +3. The handler collection is initialized. +4. Client initialize callbacks run. +5. The JSON-RPC connection opens. +6. The client sends `initialize` to the server and stores the returned `InitializeResult`. +7. The serializer is updated with server capabilities. +8. Initialized callbacks run. +9. The receiver is marked initialized. +10. Static server capabilities are registered with the client registration manager. +11. The client sends the `initialized` notification. +12. Started callbacks run and the client is marked started. + +## Capabilities and registration + +LSP capability negotiation is split across three concepts: + +- **Client capabilities** describe what the peer can handle. They are sent in `initialize`. +- **Server capabilities** describe statically available server features. They are returned in `InitializeResult`. +- **Registration options** describe per-feature routing and behavior, such as document selectors, trigger characters, command names, and work-done progress. + +Feature model types declare their capability and registration option types with attributes. Registration-option converters know how to turn server registration options into the corresponding `ServerCapabilities` fields for static registration. + +The server avoids mixing static and dynamic registration for the same feature. When a client supports dynamic registration for a capability, server handlers for that capability are not emitted into `ServerCapabilities`; instead `LanguageServerHelpers.DynamicallyRegisterHandlers(...)` sends `client/registerCapability` after initialization. When dynamic registration is not supported, registration options are converted into static server capability properties. + +Dynamic registrations are disposable. When a dynamically added handler registration is disposed, the server sends `client/unregisterCapability` for the corresponding registration IDs. + +## Source generation + +`src\JsonRpc.Generators` removes most of the repetitive protocol plumbing. Attributes on protocol models drive generated code: + +- `[GenerateHandler]` creates typed handler interfaces and abstract base classes. +- `[GenerateHandlerMethods]` creates delegate-based registration helpers such as `OnCompletion(...)`. +- `[GenerateRequestMethods]` creates strongly typed proxy methods such as `RequestCompletion(...)` or notification send helpers. +- LSP-specific attributes add capability, registration, and resolver metadata. +- Additional generators create container helpers, typed data helpers, registration option glue, and enum-like string support. + +This means adding or updating an LSP feature usually starts by defining the protocol model and annotating it correctly. The public handler and proxy APIs are then produced consistently at compile time. + +## Dependency injection and composition + +The runtime uses DryIoc internally while exposing Microsoft.Extensions.DependencyInjection-style APIs. Each facade has two creation modes: + +- Standalone factory methods such as `LanguageServer.Create(options => { ... })`. +- Service collection extensions such as `services.AddLanguageServer(...)` and `services.AddLanguageClient(...)`. + +`LanguageProtocolServiceCollectionExtensions.AddLanguageProtocolInternals(...)` wires the shared LSP services: + +- JSON-RPC core services, +- `LspSerializer`, +- `SupportedCapabilities`, +- `TextDocumentIdentifiers`, +- `LspRequestRouter`, +- `SharedHandlerCollection`, +- `ResponseRouter`, +- `ProgressManager`, +- registration option converters discovered from assemblies. + +Options can add assemblies for scanning. Assembly scanning is used to find generated metadata, capability keys, handler descriptors, and registration option converters, especially for custom protocol extensions. + +## Protocol facades and proxies + +`LanguageServer` and `LanguageClient` expose grouped protocol facades for LSP domains: + +- `TextDocument` +- `NotebookDocument` +- `Workspace` +- `Window` +- `General` +- `Client` + +These facades are strongly typed proxy surfaces over the underlying JSON-RPC response router. Generated request methods use the proxy interfaces to send protocol requests and notifications with the correct method names and result types. + +Handlers should not directly depend on `ILanguageServer` or `ILanguageClient`, because handlers are resolved during initialization. They can instead depend on facade interfaces such as `ILanguageServerFacade` or `ILanguageClientFacade`, which expose the relevant protocol surface safely. + +## Progress, work done, and partial results + +The protocol layer includes abstractions for LSP progress patterns: + +- work-done progress support through server/client work-done managers, +- partial result handling for requests that stream results, +- handler base classes for partial results and partial results with initial values, +- serializer support for progress tokens. + +Features declare progress participation by implementing interfaces such as `IWorkDoneProgressParams`, `IPartialItemsRequest`, `IPartialItemRequest`, or related generated/auto-implemented interfaces. The source generators and abstract handler bases turn those protocol shapes into handler APIs. + +## Configuration and workspace support + +The server wraps `workspace/didChangeConfiguration` and `workspace/configuration` into `ILanguageServerConfiguration`, which also implements `Microsoft.Extensions.Configuration.IConfiguration`. Server options can track configuration sections and items, query stateless configuration, and create scoped document configuration views. + +Workspace folders are handled by client and server workspace folder managers. During client initialization, the current workspace folders are included in `InitializeParams`; after startup, workspace folder capability and notifications are routed through the normal handler infrastructure. + +## Extensibility + +Custom protocol extensions use the same machinery as built-in LSP features: + +1. Define request or notification parameter models. +2. Add `[Method]`, direction, process type, and generator attributes. +3. Add capability and registration option types when needed. +4. Provide registration option converters for static server capabilities. +5. Include the assembly through options so descriptor and converter scanning can find the extension. + +Because the transport, serializer, handler registry, and routers are shared, custom methods can participate in normal request dispatch, dynamic registration, capability negotiation, and generated proxy APIs. diff --git a/src/Client/Client.csproj b/src/Client/Client.csproj index ccd049942..fa7efad30 100644 --- a/src/Client/Client.csproj +++ b/src/Client/Client.csproj @@ -1,7 +1,7 @@  - netstandard2.1;netstandard2.0;net6.0;net8.0 + $(LibraryTargetFrameworks) AnyCPU OmniSharp.Extensions.LanguageClient OmniSharp.Extensions.LanguageServer.Client diff --git a/src/Client/LanguageClient.cs b/src/Client/LanguageClient.cs index 22c7004df..1b5711482 100644 --- a/src/Client/LanguageClient.cs +++ b/src/Client/LanguageClient.cs @@ -446,7 +446,7 @@ private Supports UseOrTryAndFindCapability(Supports supports) where T : public IObservable Start => _initializeComplete.AsObservable(); - bool IResponseRouter.TryGetRequest(long id, [NotNullWhen(true)] out string? method, [NotNullWhen(true)] out TaskCompletionSource? pendingTask) + bool IResponseRouter.TryGetRequest(object id, [NotNullWhen(true)] out string? method, [NotNullWhen(true)] out TaskCompletionSource? pendingTask) { return _responseRouter.TryGetRequest(id, out method, out pendingTask); } diff --git a/src/Dap.Client/Dap.Client.csproj b/src/Dap.Client/Dap.Client.csproj index c06983a14..8b363d4d5 100644 --- a/src/Dap.Client/Dap.Client.csproj +++ b/src/Dap.Client/Dap.Client.csproj @@ -1,7 +1,7 @@  - netstandard2.1;netstandard2.0;net6.0;net8.0 + $(LibraryTargetFrameworks) AnyCPU OmniSharp.Extensions.DebugAdapter.Client OmniSharp.Extensions.DebugAdapter.Client diff --git a/src/Dap.Protocol.Proposals/Dap.Protocol.Proposals.csproj b/src/Dap.Protocol.Proposals/Dap.Protocol.Proposals.csproj index 81dde6036..d80efe2a9 100644 --- a/src/Dap.Protocol.Proposals/Dap.Protocol.Proposals.csproj +++ b/src/Dap.Protocol.Proposals/Dap.Protocol.Proposals.csproj @@ -1,7 +1,7 @@  - netstandard2.1;netstandard2.0;net6.0;net8.0 + $(LibraryTargetFrameworks) AnyCPU OmniSharp.Extensions.DebugAdapter.Proposals OmniSharp.Extensions.DebugAdapter.Protocol diff --git a/src/Dap.Protocol/Dap.Protocol.csproj b/src/Dap.Protocol/Dap.Protocol.csproj index 091aff8e0..40f8084e5 100644 --- a/src/Dap.Protocol/Dap.Protocol.csproj +++ b/src/Dap.Protocol/Dap.Protocol.csproj @@ -1,7 +1,7 @@  - netstandard2.1;netstandard2.0;net6.0;net8.0 + $(LibraryTargetFrameworks) AnyCPU OmniSharp.Extensions.DebugAdapter OmniSharp.Extensions.DebugAdapter.Protocol diff --git a/src/Dap.Protocol/IDebugAdapterProtocolProxy.cs b/src/Dap.Protocol/IDebugAdapterProtocolProxy.cs index 0eb31f5b7..1025690d8 100644 --- a/src/Dap.Protocol/IDebugAdapterProtocolProxy.cs +++ b/src/Dap.Protocol/IDebugAdapterProtocolProxy.cs @@ -59,7 +59,7 @@ public Task SendRequest(IRequest request, Cance return _responseRouter.SendRequest(request, cancellationToken); } - bool IResponseRouter.TryGetRequest(long id, [NotNullWhen(true)] out string? method, [NotNullWhen(true)] out TaskCompletionSource? pendingTask) + bool IResponseRouter.TryGetRequest(object id, [NotNullWhen(true)] out string? method, [NotNullWhen(true)] out TaskCompletionSource? pendingTask) { return _responseRouter.TryGetRequest(id, out method, out pendingTask); } diff --git a/src/Dap.Server/Dap.Server.csproj b/src/Dap.Server/Dap.Server.csproj index 767ab0412..08d48eb0d 100644 --- a/src/Dap.Server/Dap.Server.csproj +++ b/src/Dap.Server/Dap.Server.csproj @@ -1,7 +1,7 @@ - netstandard2.1;netstandard2.0;net6.0;net8.0 + $(LibraryTargetFrameworks) AnyCPU OmniSharp.Extensions.DebugAdapter.Server OmniSharp.Extensions.DebugAdapter.Server diff --git a/src/Dap.Shared/Dap.Shared.csproj b/src/Dap.Shared/Dap.Shared.csproj index 0114080b8..8938b335e 100644 --- a/src/Dap.Shared/Dap.Shared.csproj +++ b/src/Dap.Shared/Dap.Shared.csproj @@ -1,7 +1,7 @@ - netstandard2.1;netstandard2.0;net6.0;net8.0 + $(LibraryTargetFrameworks) AnyCPU OmniSharp.Extensions.DebugAdapter.Shared OmniSharp.Extensions.DebugAdapter.Shared diff --git a/src/Dap.Shared/DapResponseRouter.cs b/src/Dap.Shared/DapResponseRouter.cs index 3c1bf3d41..cacb0188c 100644 --- a/src/Dap.Shared/DapResponseRouter.cs +++ b/src/Dap.Shared/DapResponseRouter.cs @@ -16,8 +16,8 @@ public class DapResponseRouter : IResponseRouter internal readonly IOutputHandler OutputHandler; internal readonly ISerializer Serializer; - internal readonly ConcurrentDictionary pendingTask)> Requests = - new ConcurrentDictionary pendingTask)>(); + internal readonly ConcurrentDictionary pendingTask)> Requests = + new ConcurrentDictionary pendingTask)>(); internal static readonly ConcurrentDictionary MethodCache = new ConcurrentDictionary(); @@ -69,7 +69,7 @@ public IResponseRouterReturns SendRequest(string method, T @params) return new ResponseRouterReturnsImpl(this, method, @params); } - public bool TryGetRequest(long id, [NotNullWhen(true)] out string? method, [NotNullWhen(true)] out TaskCompletionSource? pendingTask) + public bool TryGetRequest(object id, [NotNullWhen(true)] out string? method, [NotNullWhen(true)] out TaskCompletionSource? pendingTask) { var result = Requests.TryGetValue(id, out var source); method = source.method; diff --git a/src/Dap.Testing/Dap.Testing.csproj b/src/Dap.Testing/Dap.Testing.csproj index 315e99a5d..f661d0e0c 100644 --- a/src/Dap.Testing/Dap.Testing.csproj +++ b/src/Dap.Testing/Dap.Testing.csproj @@ -1,7 +1,7 @@  - netstandard2.1;netstandard2.0;net6.0;net8.0 + $(LibraryTargetFrameworks) AnyCPU OmniSharp.Extensions.DebugAdapter.Testing OmniSharp.Extensions.DebugAdapter.Testing diff --git a/src/JsonRpc.Testing/JsonRpc.Testing.csproj b/src/JsonRpc.Testing/JsonRpc.Testing.csproj index 9684fe254..36f58c87f 100644 --- a/src/JsonRpc.Testing/JsonRpc.Testing.csproj +++ b/src/JsonRpc.Testing/JsonRpc.Testing.csproj @@ -1,7 +1,7 @@  - netstandard2.1;netstandard2.0;net6.0;net8.0 + $(LibraryTargetFrameworks) AnyCPU OmniSharp.Extensions.JsonRpc.Testing OmniSharp.Extensions.JsonRpc.Testing diff --git a/src/JsonRpc/DefaultJsonRpcServerFacade.cs b/src/JsonRpc/DefaultJsonRpcServerFacade.cs index 81f6c0a27..308a12834 100644 --- a/src/JsonRpc/DefaultJsonRpcServerFacade.cs +++ b/src/JsonRpc/DefaultJsonRpcServerFacade.cs @@ -50,7 +50,7 @@ public Task SendRequest(IRequest request, Cance return _responseRouter.SendRequest(request, cancellationToken); } - bool IResponseRouter.TryGetRequest(long id, [NotNullWhen(true)] out string? method, [NotNullWhen(true)] out TaskCompletionSource? pendingTask) + bool IResponseRouter.TryGetRequest(object id, [NotNullWhen(true)] out string? method, [NotNullWhen(true)] out TaskCompletionSource? pendingTask) { return _responseRouter.TryGetRequest(id, out method, out pendingTask); } diff --git a/src/JsonRpc/IResponseRouter.cs b/src/JsonRpc/IResponseRouter.cs index a66d1a6d1..2ff9d1b55 100644 --- a/src/JsonRpc/IResponseRouter.cs +++ b/src/JsonRpc/IResponseRouter.cs @@ -14,6 +14,6 @@ public interface IResponseRouter IResponseRouterReturns SendRequest(string method, T @params); IResponseRouterReturns SendRequest(string method); Task SendRequest(IRequest request, CancellationToken cancellationToken); - bool TryGetRequest(long id, [NotNullWhen(true)] out string? method, [NotNullWhen(true)] out TaskCompletionSource? pendingTask); + bool TryGetRequest(object id, [NotNullWhen(true)] out string? method, [NotNullWhen(true)] out TaskCompletionSource? pendingTask); } } diff --git a/src/JsonRpc/InputHandler.cs b/src/JsonRpc/InputHandler.cs index cf733ebd0..3d68cd156 100644 --- a/src/JsonRpc/InputHandler.cs +++ b/src/JsonRpc/InputHandler.cs @@ -323,6 +323,7 @@ internal async Task ProcessInputStream(CancellationToken cancellationToken) try { var headersParsed = false; + var invalidContentType = false; long length = 0; do { @@ -337,6 +338,7 @@ internal async Task ProcessInputStream(CancellationToken cancellationToken) { if (TryParseHeaders(ref buffer, out var line)) { + invalidContentType = !IsSupportedContentType(line); if (TryParseContentLength(ref line, out length)) { headersParsed = true; @@ -346,7 +348,16 @@ internal async Task ProcessInputStream(CancellationToken cancellationToken) if (headersParsed && length == 0) { - HandleRequest(new ReadOnlySequence(Array.Empty())); + if (invalidContentType) + { + _outputHandler.Send(new ParseError(string.Empty)); + invalidContentType = false; + } + else + { + HandleRequest(new ReadOnlySequence(Array.Empty())); + } + headersParsed = false; } @@ -356,7 +367,16 @@ internal async Task ProcessInputStream(CancellationToken cancellationToken) { headersParsed = false; length = 0; - HandleRequest(line); + if (invalidContentType) + { + _outputHandler.Send(new ParseError(string.Empty)); + invalidContentType = false; + } + else + { + HandleRequest(line); + } + dataParsed = true; } } @@ -384,6 +404,41 @@ internal async Task ProcessInputStream(CancellationToken cancellationToken) } } + private static bool IsSupportedContentType(ReadOnlySequence headers) + { + var headerText = Encoding.ASCII.GetString(headers.ToArray()); + foreach (var header in headerText.Split(new[] { "\r\n" }, StringSplitOptions.RemoveEmptyEntries)) + { + var colonIndex = header.IndexOf(':'); + if (colonIndex < 0) + { + continue; + } + + var name = header.Substring(0, colonIndex).Trim(); + if (!string.Equals(name, "Content-Type", StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + var value = header.Substring(colonIndex + 1); + foreach (var part in value.Split(';').Skip(1)) + { + var pair = part.Split(new[] { '=' }, 2); + if (pair.Length != 2 || !string.Equals(pair[0].Trim(), "charset", StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + var charset = pair[1].Trim().Trim('"'); + return string.Equals(charset, "utf-8", StringComparison.OrdinalIgnoreCase) || + string.Equals(charset, "utf8", StringComparison.OrdinalIgnoreCase); + } + } + + return true; + } + private void HandleRequest(in ReadOnlySequence request) { JToken payload; @@ -407,51 +462,46 @@ private void HandleRequest(in ReadOnlySequence request) // using (_logger.TimeDebug("InputHandler is handling the request")) // { - var (requests, hasResponse) = _receiver.GetRequests(payload); - if (hasResponse) + var (requests, _) = _receiver.GetRequests(payload); + var items = requests as Renor[] ?? requests.ToArray(); + foreach (var response in items.Where(x => x.IsResponse).Select(x => x.Response!)) { - foreach (var response in requests.Where(x => x.IsResponse).Select(x => x.Response!)) + // _logger.LogDebug("Handling Response for request {ResponseId}", response.Id); + if (response.Id is null) { - // _logger.LogDebug("Handling Response for request {ResponseId}", response.Id); - var id = response.Id is string s ? long.Parse(s) : response.Id is long l ? l : -1; - if (id < 0) - { - // _logger.LogDebug("Id was out of range, skipping request {ResponseId}", response.Id); - continue; - } + continue; + } - if (!_responseRouter.TryGetRequest(id, out var method, out var tcs)) - { - // _logger.LogDebug("Request {ResponseId} was not found in the response router, unable to complete", response.Id); - continue; - } + if (!_responseRouter.TryGetRequest(response.Id, out var method, out var tcs) && + !(response.Id is string s && long.TryParse(s, out var numericId) && _responseRouter.TryGetRequest(numericId, out method, out tcs))) + { + // _logger.LogDebug("Request {ResponseId} was not found in the response router, unable to complete", response.Id); + continue; + } - _inputQueue.OnNext( - Observable.Create( - observer => + _inputQueue.OnNext( + Observable.Create( + observer => + { + if (response is ServerResponse serverResponse) { - if (response is ServerResponse serverResponse) - { - // _logger.LogDebug("Setting successful Response for {ResponseId}", response.Id); - tcs.TrySetResult(serverResponse.Result); - } - else if (response is ServerError serverError) - { - // _logger.LogDebug("Setting error for {ResponseId}", response.Id); - tcs.TrySetException(DefaultErrorParser(method, serverError, _getException)); - } - - observer.OnCompleted(); - return Disposable.Empty; + // _logger.LogDebug("Setting successful Response for {ResponseId}", response.Id); + tcs.TrySetResult(serverResponse.Result); + } + else if (response is ServerError serverError) + { + // _logger.LogDebug("Setting error for {ResponseId}", response.Id); + tcs.TrySetException(DefaultErrorParser(method, serverError, _getException)); } - ) - ); - } - return; + observer.OnCompleted(); + return Disposable.Empty; + } + ) + ); } - foreach (var item in requests) + foreach (var item in items) { if (item.IsRequest && item.Request != null) { @@ -463,7 +513,7 @@ private void HandleRequest(in ReadOnlySequence request) { _logger.LogDebug("Request handler was not found (or not setup) {Method} {ResponseId}", item.Request.Method, item.Request.Id); _outputHandler.Send(new MethodNotFound(item.Request.Id, item.Request.Method)); - return; + continue; } var requestHandle = _requestInvoker.InvokeRequest(descriptor, item.Request); @@ -514,7 +564,7 @@ private void HandleRequest(in ReadOnlySequence request) _logger.LogDebug("Notification handler was not found (or not setup) {Method}", item.Notification.Method); // TODO: Figure out a good way to send this feedback back. // _outputHandler.Send(new RpcError(null, new ErrorMessage(-32601, $"Method not found - {item.Notification.Method}"))); - return; + continue; } _requestInvoker.InvokeNotification(descriptor, item.Notification); @@ -548,6 +598,8 @@ private static Exception DefaultErrorParser(string? method, ServerError error, C ErrorCodes.ParseError => new ParseErrorException(error.Id), ErrorCodes.RequestCancelled => new RequestCancelledException(error.Id), ErrorCodes.ContentModified => new ContentModifiedException(error.Id), + ErrorCodes.ServerCancelled => new ServerCancelledException(error.Id), + ErrorCodes.RequestFailed => new RequestFailedException(error.Id), ErrorCodes.UnknownErrorCode => new UnknownErrorException(error.Id), ErrorCodes.Exception => new JsonRpcException(ErrorCodes.Exception, error.Id, error.Error.Message, error.Error.Data?.ToString()), _ => customHandler?.Invoke(error, method ?? "UNKNOWN") ?? diff --git a/src/JsonRpc/JsonRpc.csproj b/src/JsonRpc/JsonRpc.csproj index 4cb657da7..b5841b3ac 100644 --- a/src/JsonRpc/JsonRpc.csproj +++ b/src/JsonRpc/JsonRpc.csproj @@ -1,6 +1,6 @@ - netstandard2.1;netstandard2.0;net6.0;net8.0 + $(LibraryTargetFrameworks) AnyCPU OmniSharp.Extensions.JsonRpc OmniSharp.Extensions.JsonRpc diff --git a/src/JsonRpc/JsonRpcServerBase.cs b/src/JsonRpc/JsonRpcServerBase.cs index ab7ea00b0..9353c269e 100644 --- a/src/JsonRpc/JsonRpcServerBase.cs +++ b/src/JsonRpc/JsonRpcServerBase.cs @@ -47,7 +47,7 @@ public IResponseRouterReturns SendRequest(string method) return ResponseRouter.SendRequest(method); } - bool IResponseRouter.TryGetRequest(long id, [NotNullWhen(true)] out string? method, [NotNullWhen(true)] out TaskCompletionSource? pendingTask) + bool IResponseRouter.TryGetRequest(object id, [NotNullWhen(true)] out string? method, [NotNullWhen(true)] out TaskCompletionSource? pendingTask) { return ResponseRouter.TryGetRequest(id, out method, out pendingTask); } diff --git a/src/JsonRpc/NoopResponseRouter.cs b/src/JsonRpc/NoopResponseRouter.cs index a2c3e89c5..d7e9330f5 100644 --- a/src/JsonRpc/NoopResponseRouter.cs +++ b/src/JsonRpc/NoopResponseRouter.cs @@ -44,7 +44,7 @@ public Task SendRequest(IRequest request, Cance return Task.FromResult(default!); } - bool IResponseRouter.TryGetRequest(long id, [NotNullWhen(true)] out string? method, [NotNullWhen(true)] out TaskCompletionSource? pendingTask) + bool IResponseRouter.TryGetRequest(object id, [NotNullWhen(true)] out string? method, [NotNullWhen(true)] out TaskCompletionSource? pendingTask) { method = default!; pendingTask = default!; diff --git a/src/JsonRpc/ResponseRouter.cs b/src/JsonRpc/ResponseRouter.cs index e64df4ef9..e471b3f77 100644 --- a/src/JsonRpc/ResponseRouter.cs +++ b/src/JsonRpc/ResponseRouter.cs @@ -15,8 +15,8 @@ internal class ResponseRouter : IResponseRouter internal readonly ISerializer Serializer; private readonly IHandlerTypeDescriptorProvider _handlerTypeDescriptorProvider; - internal readonly ConcurrentDictionary pendingTask)> Requests = - new ConcurrentDictionary pendingTask)>(); + internal readonly ConcurrentDictionary pendingTask)> Requests = + new ConcurrentDictionary pendingTask)>(); public ResponseRouter( Lazy outputHandler, ISerializer serializer, IHandlerTypeDescriptorProvider handlerTypeDescriptorProvider @@ -68,7 +68,7 @@ public IResponseRouterReturns SendRequest(string method, T @params) return new ResponseRouterReturnsImpl(this, method, @params); } - public bool TryGetRequest(long id, [NotNullWhen(true)] out string? method, [NotNullWhen(true)] out TaskCompletionSource? pendingTask) + public bool TryGetRequest(object id, [NotNullWhen(true)] out string? method, [NotNullWhen(true)] out TaskCompletionSource? pendingTask) { var result = Requests.TryGetValue(id, out var source); method = source.method; diff --git a/src/JsonRpc/Server/RequestFailedException.cs b/src/JsonRpc/Server/RequestFailedException.cs index 8ea568a5f..d2102ad8a 100644 --- a/src/JsonRpc/Server/RequestFailedException.cs +++ b/src/JsonRpc/Server/RequestFailedException.cs @@ -16,7 +16,7 @@ public class RequestFailedException /// The LSP / JSON-RPC request Id (if known). /// public RequestFailedException(object? requestId) - : this(ErrorCodes.RequestFailed, requestId?.ToString() ?? "(unknown)", "Content not modified.", null!) + : this(ErrorCodes.RequestFailed, requestId?.ToString() ?? "(unknown)", "Request failed.", null!) { } @@ -30,7 +30,7 @@ public RequestFailedException(object? requestId) /// The exception that caused this exception to be raised. /// public RequestFailedException(object? requestId, Exception inner) - : this(ErrorCodes.RequestFailed, requestId?.ToString() ?? "(unknown)", "Content not modified.", inner) + : this(ErrorCodes.RequestFailed, requestId?.ToString() ?? "(unknown)", "Request failed.", inner) { } diff --git a/src/JsonRpc/Server/ServerCancelledException.cs b/src/JsonRpc/Server/ServerCancelledException.cs index 8a7338928..a2c0c23c1 100644 --- a/src/JsonRpc/Server/ServerCancelledException.cs +++ b/src/JsonRpc/Server/ServerCancelledException.cs @@ -16,7 +16,7 @@ public class ServerCancelledException /// The LSP / JSON-RPC request Id (if known). /// public ServerCancelledException(object? requestId) - : this(ErrorCodes.ServerCancelled, requestId?.ToString() ?? "(unknown)", "Content not modified.", null!) + : this(ErrorCodes.ServerCancelled, requestId?.ToString() ?? "(unknown)", "Server cancelled the request.", null!) { } @@ -30,7 +30,7 @@ public ServerCancelledException(object? requestId) /// The exception that caused this exception to be raised. /// public ServerCancelledException(object? requestId, Exception inner) - : this(ErrorCodes.ServerCancelled, requestId?.ToString() ?? "(unknown)", "Content not modified.", inner) + : this(ErrorCodes.ServerCancelled, requestId?.ToString() ?? "(unknown)", "Server cancelled the request.", inner) { } diff --git a/src/JsonRpc/Server/UnknownErrorException.cs b/src/JsonRpc/Server/UnknownErrorException.cs index eeffbc9d4..60e7da71d 100644 --- a/src/JsonRpc/Server/UnknownErrorException.cs +++ b/src/JsonRpc/Server/UnknownErrorException.cs @@ -17,7 +17,7 @@ public class UnknownErrorException /// The LSP / JSON-RPC request Id (if known). /// public UnknownErrorException(object? requestId) - : base(ErrorCodes.ContentModified, requestId, "Content not modified.") + : base(ErrorCodes.UnknownErrorCode, requestId, "Unknown error.") { } diff --git a/src/Protocol.Proposals/Protocol.Proposals.csproj b/src/Protocol.Proposals/Protocol.Proposals.csproj index d56f5894f..ae00fac1b 100644 --- a/src/Protocol.Proposals/Protocol.Proposals.csproj +++ b/src/Protocol.Proposals/Protocol.Proposals.csproj @@ -1,6 +1,6 @@ - netstandard2.1;netstandard2.0;net6.0;net8.0 + $(LibraryTargetFrameworks) AnyCPU OmniSharp.Extensions.LanguageProtocol.Proposals OmniSharp.Extensions.LanguageServer.Protocol diff --git a/src/Protocol/Client/Capabilities/ITextDocumentClientCapabilities.cs b/src/Protocol/Client/Capabilities/ITextDocumentClientCapabilities.cs index 27efadef3..da4eb8c1c 100644 --- a/src/Protocol/Client/Capabilities/ITextDocumentClientCapabilities.cs +++ b/src/Protocol/Client/Capabilities/ITextDocumentClientCapabilities.cs @@ -174,5 +174,19 @@ public interface ITextDocumentClientCapabilities : ICapabilitiesBase /// @since 3.17.0 /// Supports Diagnostic { get; set; } + + /// + /// Client capabilities specific to text document filters. + /// + /// @since 3.18.0 + /// + Supports Filters { get; set; } + + /// + /// Client capabilities specific to inline completions. + /// + /// @since 3.18.0 + /// + Supports InlineCompletion { get; set; } } } diff --git a/src/Protocol/Client/Capabilities/TextDocumentClientCapabilities.cs b/src/Protocol/Client/Capabilities/TextDocumentClientCapabilities.cs index 202983903..f38a21047 100644 --- a/src/Protocol/Client/Capabilities/TextDocumentClientCapabilities.cs +++ b/src/Protocol/Client/Capabilities/TextDocumentClientCapabilities.cs @@ -174,5 +174,19 @@ public class TextDocumentClientCapabilities : CapabilitiesBase, ITextDocumentCli /// @since 3.17.0 /// public Supports Diagnostic { get; set; } + + /// + /// Client capabilities specific to text document filters. + /// + /// @since 3.18.0 + /// + public Supports Filters { get; set; } + + /// + /// Client capabilities specific to inline completions. + /// + /// @since 3.18.0 + /// + public Supports InlineCompletion { get; set; } } } diff --git a/src/Protocol/Client/Capabilities/WorkspaceClientCapabilities.cs b/src/Protocol/Client/Capabilities/WorkspaceClientCapabilities.cs index 0f8d22833..335084639 100644 --- a/src/Protocol/Client/Capabilities/WorkspaceClientCapabilities.cs +++ b/src/Protocol/Client/Capabilities/WorkspaceClientCapabilities.cs @@ -75,6 +75,20 @@ public class WorkspaceClientCapabilities : CapabilitiesBase /// public Supports Diagnostics { get; set; } + /// + /// Client workspace capabilities specific to folding ranges. + /// + /// @since 3.18.0. + /// + public Supports FoldingRange { get; set; } + + /// + /// Client workspace capabilities specific to dynamic text document content. + /// + /// @since 3.18.0. + /// + public Supports TextDocumentContent { get; set; } + /// /// The client has support for workspace folders. /// diff --git a/src/Protocol/Client/Capabilities/WorkspaceEditCapability.cs b/src/Protocol/Client/Capabilities/WorkspaceEditCapability.cs index 78028f9e2..40fa2d243 100644 --- a/src/Protocol/Client/Capabilities/WorkspaceEditCapability.cs +++ b/src/Protocol/Client/Capabilities/WorkspaceEditCapability.cs @@ -46,5 +46,21 @@ public class WorkspaceEditCapability : ICapability /// [Optional] public WorkspaceEditSupportCapabilitiesChangeAnnotationSupport? ChangeAnnotationSupport { get; set; } + + /// + /// Whether the client supports metadata on workspace edits. + /// + /// @since 3.18.0 + /// + [Optional] + public bool MetadataSupport { get; set; } + + /// + /// Whether the client supports snippets in text document edits. + /// + /// @since 3.18.0 + /// + [Optional] + public bool SnippetEditSupport { get; set; } } } diff --git a/src/Protocol/Features/Document/CodeActionFeature.cs b/src/Protocol/Features/Document/CodeActionFeature.cs index 36ca7aac9..fe5dcef88 100644 --- a/src/Protocol/Features/Document/CodeActionFeature.cs +++ b/src/Protocol/Features/Document/CodeActionFeature.cs @@ -333,6 +333,14 @@ public partial class CodeActionRegistrationOptions : IWorkDoneProgressOptions, I [Optional] public bool ResolveProvider { get; set; } + /// + /// Documentation for code action kinds provided by this server. + /// + /// @since 3.18.0 + /// + [Optional] + public Container? Documentation { get; set; } + private class CodeActionRegistrationOptionsConverter : RegistrationOptionsConverterBase { private readonly IHandlersManager _handlersManager; @@ -349,11 +357,24 @@ public override StaticOptions Convert(CodeActionRegistrationOptions source) CodeActionKinds = source.CodeActionKinds, ResolveProvider = source.ResolveProvider || _handlersManager.Descriptors.Any(z => z.HandlerType == typeof(ICodeActionResolveHandler)), WorkDoneProgress = source.WorkDoneProgress, + Documentation = source.Documentation, }; } } } + /// + /// Documentation for a class of code actions. + /// + /// @since 3.18.0 + /// + public record CodeActionKindDocumentation + { + public CodeActionKind Kind { get; init; } + + public Command Command { get; init; } = null!; + } + /// /// A set of predefined code action kinds /// @@ -414,6 +435,13 @@ public readonly partial struct CodeActionKind /// public static CodeActionKind RefactorRewrite { get; } = new CodeActionKind("refactor.rewrite"); + /// + /// Base kind for refactoring move actions: 'refactor.move' + /// + /// @since 3.18.0 + /// + public static CodeActionKind RefactorMove { get; } = new CodeActionKind("refactor.move"); + /// /// Base kind for source actions: `source` /// @@ -436,6 +464,13 @@ public readonly partial struct CodeActionKind /// @since 3.17.0 /// public static CodeActionKind SourceFixAll { get; } = new CodeActionKind("source.fixAll"); + + /// + /// Base kind for notebook actions: `notebook`. + /// + /// @since 3.18.0 + /// + public static CodeActionKind Notebook { get; } = new CodeActionKind("notebook"); } @@ -515,6 +550,14 @@ public partial class CodeActionCapability : DynamicCapability /// [Optional] public bool HonorsChangeAnnotations { get; set; } + + /// + /// Whether the client supports documentation for code action kinds. + /// + /// @since 3.18.0 + /// + [Optional] + public bool DocumentationSupport { get; set; } } public class CodeActionLiteralSupportOptions diff --git a/src/Protocol/Features/Document/CodeLensFeature.cs b/src/Protocol/Features/Document/CodeLensFeature.cs index 6c6aac133..3d0ef76f9 100644 --- a/src/Protocol/Features/Document/CodeLensFeature.cs +++ b/src/Protocol/Features/Document/CodeLensFeature.cs @@ -8,6 +8,7 @@ using OmniSharp.Extensions.LanguageServer.Protocol.Client.Capabilities; using OmniSharp.Extensions.LanguageServer.Protocol.Document; using OmniSharp.Extensions.LanguageServer.Protocol.Generation; +using OmniSharp.Extensions.LanguageServer.Protocol.Models; using OmniSharp.Extensions.LanguageServer.Protocol.Serialization; using OmniSharp.Extensions.LanguageServer.Protocol.Server; using OmniSharp.Extensions.LanguageServer.Protocol.Server.Capabilities; @@ -126,6 +127,21 @@ namespace Client.Capabilities [CapabilityKey(nameof(ClientCapabilities.TextDocument), nameof(TextDocumentClientCapabilities.CodeLens))] public partial class CodeLensCapability : DynamicCapability { + /// + /// Indicates which properties a client can resolve lazily on a code lens. + /// + /// @since 3.18.0 + /// + [Optional] + public CodeLensCapabilityResolveSupport? ResolveSupport { get; set; } + } + + public class CodeLensCapabilityResolveSupport + { + /// + /// The properties that a client can resolve lazily. + /// + public Container Properties { get; set; } = null!; } /// diff --git a/src/Protocol/Features/Document/CompletionFeature.cs b/src/Protocol/Features/Document/CompletionFeature.cs index 23b23698d..b266c6fb2 100644 --- a/src/Protocol/Features/Document/CompletionFeature.cs +++ b/src/Protocol/Features/Document/CompletionFeature.cs @@ -2,8 +2,10 @@ using System.Diagnostics; using System.Linq; using System.Reflection; +using System.Runtime.Serialization; using MediatR; using Newtonsoft.Json; +using Newtonsoft.Json.Converters; using Newtonsoft.Json.Linq; using OmniSharp.Extensions.JsonRpc; using OmniSharp.Extensions.JsonRpc.Generation; @@ -443,12 +445,21 @@ public CompletionList(IEnumerable items, bool isIncomplete) : ba [Optional] public CompletionListItemDefaults? ItemDefaults { get; set; } + /// + /// Specifies how fields from a completion item should be combined with those from `itemDefaults`. + /// + /// @since 3.18.0 + /// + [Optional] + public CompletionItemApplyKinds? ApplyKind { get; set; } + public static CompletionList? From(CompletionList? list) where T : class?, IHandlerIdentity? => list switch { not null => new(list.Items.Select(CompletionItem.From)!, list.IsIncomplete) { - ItemDefaults = list.ItemDefaults + ItemDefaults = list.ItemDefaults, + ApplyKind = list.ApplyKind }, _ => null }; @@ -456,14 +467,15 @@ public CompletionList(IEnumerable items, bool isIncomplete) : ba public static CompletionList From(CompletionList? source, IEnumerable? result) => new((source?.Items ?? Array.Empty()).Concat(result ?? Array.Empty())) { - ItemDefaults = source?.ItemDefaults + ItemDefaults = source?.ItemDefaults, + ApplyKind = source?.ApplyKind }; internal class Converter : JsonConverter { public override void WriteJson(JsonWriter writer, CompletionList? value, JsonSerializer serializer) { - if (!value.IsIncomplete && value.ItemDefaults is null) + if (!value.IsIncomplete && value.ItemDefaults is null && value.ApplyKind is null) { serializer.Serialize(writer, value.Items.ToArray()); return; @@ -487,6 +499,12 @@ public override void WriteJson(JsonWriter writer, CompletionList? value, JsonSer serializer.Serialize(writer, value.ItemDefaults); } + if (value.ApplyKind is { }) + { + writer.WritePropertyName("applyKind"); + serializer.Serialize(writer, value.ApplyKind); + } + writer.WriteEndObject(); } @@ -509,7 +527,8 @@ public override void WriteJson(JsonWriter writer, CompletionList? value, JsonSer var items = result["items"].ToObject>(serializer); return new CompletionList(items, result["isIncomplete"]?.Value() ?? false) { - ItemDefaults = result["itemDefaults"]?.ToObject(serializer) + ItemDefaults = result["itemDefaults"]?.ToObject(serializer), + ApplyKind = result["applyKind"]?.ToObject(serializer) }; } @@ -564,13 +583,22 @@ public CompletionList(bool isIncomplete, params CompletionItem[] items) : bas [Optional] public CompletionListItemDefaults? ItemDefaults { get; set; } + /// + /// Specifies how fields from a completion item should be combined with those from `itemDefaults`. + /// + /// @since 3.18.0 + /// + [Optional] + public CompletionItemApplyKinds? ApplyKind { get; set; } + public static CompletionList? Create(CompletionList? list) => list switch { not null => new(list.IsIncomplete, list.Items.Select(CompletionItem.From)!) { - ItemDefaults = list.ItemDefaults + ItemDefaults = list.ItemDefaults, + ApplyKind = list.ApplyKind }, _ => null }; @@ -580,7 +608,8 @@ public CompletionList(bool isIncomplete, params CompletionItem[] items) : bas { not null => new CompletionList(container.Select(value => (CompletionItem)value), container.IsIncomplete) { - ItemDefaults = container.ItemDefaults + ItemDefaults = container.ItemDefaults, + ApplyKind = container.ApplyKind }, _ => null }; @@ -651,6 +680,35 @@ public record CompletionListItemDefaults public JToken? Data { get; init; } } + /// + /// Specifies how a completion item's fields combine with completion list item defaults. + /// + /// @since 3.18.0 + /// + public record CompletionItemApplyKinds + { + [Optional] + public ApplyKind? CommitCharacters { get; init; } + + [Optional] + public ApplyKind? Data { get; init; } + } + + /// + /// Defines whether a completion item field replaces or merges with a completion list item default. + /// + /// @since 3.18.0 + /// + [JsonConverter(typeof(StringEnumConverter))] + public enum ApplyKind + { + [EnumMember(Value = "replace")] + Replace, + + [EnumMember(Value = "merge")] + Merge + } + /// /// Additional details for a completion item label. /// @@ -739,6 +797,14 @@ public class CompletionListCapabilityOptions /// [Optional] public Container? ItemDefaults { get; set; } + + /// + /// The client supports the `applyKind` property on completion lists. + /// + /// @since 3.18.0 + /// + [Optional] + public bool ApplyKindSupport { get; set; } } public class CompletionItemCapabilityOptions diff --git a/src/Protocol/Features/Document/DiagnosticsFeature.cs b/src/Protocol/Features/Document/DiagnosticsFeature.cs index 3fcbae608..f12d6eebb 100644 --- a/src/Protocol/Features/Document/DiagnosticsFeature.cs +++ b/src/Protocol/Features/Document/DiagnosticsFeature.cs @@ -1,6 +1,7 @@ using System.Collections.Immutable; using MediatR; using Newtonsoft.Json; +using Newtonsoft.Json.Linq; using OmniSharp.Extensions.JsonRpc; using OmniSharp.Extensions.JsonRpc.Generation; using OmniSharp.Extensions.LanguageServer.Protocol.Client; @@ -72,7 +73,7 @@ public partial record WorkspaceDiagnosticParams : IWorkDoneProgressParams, [GenerateHandler("OmniSharp.Extensions.LanguageServer.Protocol.Workspace")] [GenerateHandlerMethods] [GenerateRequestMethods(typeof(IWorkspaceLanguageServer), typeof(ILanguageServer))] - [Capability(typeof(CodeLensWorkspaceClientCapabilities))] + [Capability(typeof(DiagnosticWorkspaceClientCapabilities))] public partial record DiagnosticRefreshParams : IRequest; public interface IDiagnosticReport @@ -123,14 +124,19 @@ internal class Converter : JsonConverter { public override void WriteJson(JsonWriter writer, RelatedDocumentDiagnosticReport? value, JsonSerializer serializer) { - throw new NotImplementedException(); + WorkspaceDocumentDiagnosticReport.DiagnosticReportConverter.WriteRelatedDocumentDiagnosticReport(writer, value, serializer); } public override RelatedDocumentDiagnosticReport ReadJson( JsonReader reader, Type objectType, RelatedDocumentDiagnosticReport? existingValue, bool hasExistingValue, JsonSerializer serializer ) { - throw new NotImplementedException(); + if (reader.TokenType == JsonToken.Null) + { + return null!; + } + + return WorkspaceDocumentDiagnosticReport.DiagnosticReportConverter.ReadRelatedDocumentDiagnosticReport(JObject.Load(reader), serializer); } } } @@ -147,14 +153,19 @@ internal class Converter : JsonConverter { public override void WriteJson(JsonWriter writer, DocumentDiagnosticReport value, JsonSerializer serializer) { - throw new NotImplementedException(); + WorkspaceDocumentDiagnosticReport.DiagnosticReportConverter.WriteDocumentDiagnosticReport(writer, value, serializer); } public override DocumentDiagnosticReport ReadJson( JsonReader reader, Type objectType, DocumentDiagnosticReport existingValue, bool hasExistingValue, JsonSerializer serializer ) { - throw new NotImplementedException(); + if (reader.TokenType == JsonToken.Null) + { + return null!; + } + + return WorkspaceDocumentDiagnosticReport.DiagnosticReportConverter.ReadDocumentDiagnosticReport(JObject.Load(reader), serializer); } } } @@ -306,11 +317,11 @@ public partial record DocumentDiagnosticReportPartialResult /// public ImmutableDictionary? RelatedDocuments { get; init; } - public static DocumentDiagnosticReportPartialResult? From(DocumentDiagnosticReport? result) + public static DocumentDiagnosticReportPartialResult? From(RelatedDocumentDiagnosticReport? result) { - if (result is null) + if (result is not null) { - return null; + return new DocumentDiagnosticReportPartialResult { RelatedDocuments = result.RelatedDocuments }; } return null; @@ -397,14 +408,256 @@ internal class Converter : JsonConverter { public override void WriteJson(JsonWriter writer, WorkspaceDocumentDiagnosticReport value, JsonSerializer serializer) { - throw new NotImplementedException(); + DiagnosticReportConverter.WriteWorkspaceDocumentDiagnosticReport(writer, value, serializer); } public override WorkspaceDocumentDiagnosticReport ReadJson( JsonReader reader, Type objectType, WorkspaceDocumentDiagnosticReport existingValue, bool hasExistingValue, JsonSerializer serializer ) { - throw new NotImplementedException(); + if (reader.TokenType == JsonToken.Null) + { + return null!; + } + + return DiagnosticReportConverter.ReadWorkspaceDocumentDiagnosticReport(JObject.Load(reader), serializer); + } + } + + internal static class DiagnosticReportConverter + { + public static DocumentDiagnosticReportKind GetKind(JObject result) + { + var kind = result["kind"]?.Value(); + if (string.IsNullOrWhiteSpace(kind)) + { + throw new JsonSerializationException("Diagnostic report is missing a kind."); + } + + return new DocumentDiagnosticReportKind(kind); + } + + public static DocumentDiagnosticReport ReadDocumentDiagnosticReport(JObject result, JsonSerializer serializer) + { + return GetKind(result) switch { + var kind when kind == DocumentDiagnosticReportKind.Full => new FullDocumentDiagnosticReport { + ResultId = result["resultId"]?.Value(), + Items = ReadItems(result, serializer) + }, + var kind when kind == DocumentDiagnosticReportKind.Unchanged => new UnchangedDocumentDiagnosticReport { + ResultId = ReadRequiredString(result, "resultId") + }, + var kind => throw new JsonSerializationException($"Unknown diagnostic report kind '{kind}'") + }; + } + + public static RelatedDocumentDiagnosticReport ReadRelatedDocumentDiagnosticReport(JObject result, JsonSerializer serializer) + { + return GetKind(result) switch { + var kind when kind == DocumentDiagnosticReportKind.Full => new RelatedFullDocumentDiagnosticReport { + ResultId = result["resultId"]?.Value(), + Items = ReadItems(result, serializer), + RelatedDocuments = ReadRelatedDocuments(result, serializer) + }, + var kind when kind == DocumentDiagnosticReportKind.Unchanged => new RelatedUnchangedDocumentDiagnosticReport { + ResultId = ReadRequiredString(result, "resultId"), + RelatedDocuments = ReadRelatedDocuments(result, serializer) + }, + var kind => throw new JsonSerializationException($"Unknown diagnostic report kind '{kind}'") + }; + } + + public static WorkspaceDocumentDiagnosticReport ReadWorkspaceDocumentDiagnosticReport(JObject result, JsonSerializer serializer) + { + return GetKind(result) switch { + var kind when kind == DocumentDiagnosticReportKind.Full => new WorkspaceFullDocumentDiagnosticReport { + Uri = ReadRequired(result, "uri", serializer), + Version = result["version"]?.Type == JTokenType.Null ? null : result["version"]?.Value(), + ResultId = result["resultId"]?.Value(), + Items = ReadItems(result, serializer) + }, + var kind when kind == DocumentDiagnosticReportKind.Unchanged => new WorkspaceUnchangedDocumentDiagnosticReport { + Uri = ReadRequired(result, "uri", serializer), + Version = result["version"]?.Type == JTokenType.Null ? null : result["version"]?.Value(), + ResultId = ReadRequiredString(result, "resultId") + }, + var kind => throw new JsonSerializationException($"Unknown workspace diagnostic report kind '{kind}'") + }; + } + + public static void WriteDocumentDiagnosticReport(JsonWriter writer, DocumentDiagnosticReport? value, JsonSerializer serializer) + { + switch (value) + { + case null: + writer.WriteNull(); + return; + case FullDocumentDiagnosticReport full: + WriteFull(writer, full, serializer); + return; + case UnchangedDocumentDiagnosticReport unchanged: + WriteUnchanged(writer, unchanged); + return; + default: + throw new JsonSerializationException($"Unknown diagnostic report type {value.GetType()}"); + } + } + + public static void WriteRelatedDocumentDiagnosticReport(JsonWriter writer, RelatedDocumentDiagnosticReport? value, JsonSerializer serializer) + { + switch (value) + { + case null: + writer.WriteNull(); + return; + case RelatedFullDocumentDiagnosticReport full: + WriteStartObject(writer, full.Kind); + WriteOptionalString(writer, "resultId", full.ResultId); + WriteProperty(writer, "items", full.Items, serializer); + WriteRelatedDocuments(writer, full.RelatedDocuments, serializer); + writer.WriteEndObject(); + return; + case RelatedUnchangedDocumentDiagnosticReport unchanged: + WriteStartObject(writer, unchanged.Kind); + WriteProperty(writer, "resultId", unchanged.ResultId, serializer); + WriteRelatedDocuments(writer, unchanged.RelatedDocuments, serializer); + writer.WriteEndObject(); + return; + default: + throw new JsonSerializationException($"Unknown related diagnostic report type {value.GetType()}"); + } + } + + public static void WriteWorkspaceDocumentDiagnosticReport(JsonWriter writer, WorkspaceDocumentDiagnosticReport? value, JsonSerializer serializer) + { + switch (value) + { + case null: + writer.WriteNull(); + return; + case WorkspaceFullDocumentDiagnosticReport full: + WriteStartObject(writer, full.Kind); + WriteProperty(writer, "uri", full.Uri, serializer); + WriteProperty(writer, "version", full.Version, serializer); + WriteOptionalString(writer, "resultId", full.ResultId); + WriteProperty(writer, "items", full.Items, serializer); + writer.WriteEndObject(); + return; + case WorkspaceUnchangedDocumentDiagnosticReport unchanged: + WriteStartObject(writer, unchanged.Kind); + WriteProperty(writer, "uri", unchanged.Uri, serializer); + WriteProperty(writer, "version", unchanged.Version, serializer); + WriteProperty(writer, "resultId", unchanged.ResultId, serializer); + writer.WriteEndObject(); + return; + default: + throw new JsonSerializationException($"Unknown workspace diagnostic report type {value.GetType()}"); + } + } + + private static void WriteFull(JsonWriter writer, IFullDocumentDiagnosticReport full, JsonSerializer serializer) + { + WriteStartObject(writer, full.Kind); + WriteOptionalString(writer, "resultId", full.ResultId); + WriteProperty(writer, "items", full.Items, serializer); + writer.WriteEndObject(); + } + + private static void WriteUnchanged(JsonWriter writer, IUnchangedDocumentDiagnosticReport unchanged) + { + WriteStartObject(writer, unchanged.Kind); + WriteProperty(writer, "resultId", unchanged.ResultId, null); + writer.WriteEndObject(); + } + + private static void WriteStartObject(JsonWriter writer, DocumentDiagnosticReportKind kind) + { + writer.WriteStartObject(); + writer.WritePropertyName("kind"); + writer.WriteValue(kind.ToString()); + } + + private static void WriteRelatedDocuments( + JsonWriter writer, ImmutableDictionary? relatedDocuments, JsonSerializer serializer + ) + { + if (relatedDocuments is null || relatedDocuments.Count == 0) + { + return; + } + + writer.WritePropertyName("relatedDocuments"); + writer.WriteStartObject(); + foreach (var item in relatedDocuments) + { + writer.WritePropertyName(item.Key.ToString()); + WriteDocumentDiagnosticReport(writer, item.Value, serializer); + } + + writer.WriteEndObject(); + } + + private static ImmutableDictionary? ReadRelatedDocuments(JObject result, JsonSerializer serializer) + { + if (result["relatedDocuments"] is not JObject relatedDocuments) + { + return null; + } + + var builder = ImmutableDictionary.Empty.ToBuilder(); + foreach (var property in relatedDocuments.Properties()) + { + builder.Add(DocumentUri.Parse(property.Name), ReadDocumentDiagnosticReport((JObject)property.Value, serializer)); + } + + return builder.ToImmutable(); + } + + private static Container ReadItems(JObject result, JsonSerializer serializer) + { + return result["items"]?.ToObject>(serializer) ?? new Container(); + } + + private static string ReadRequiredString(JObject result, string name) + { + return result[name]?.Value() ?? + throw new JsonSerializationException($"Diagnostic report is missing required property '{name}'."); + } + + private static T ReadRequired(JObject result, string name, JsonSerializer serializer) + { + var token = result[name] ?? + throw new JsonSerializationException($"Diagnostic report is missing required property '{name}'."); + var value = token.ToObject(serializer); + if (value is null) + { + throw new JsonSerializationException($"Diagnostic report property '{name}' cannot be null."); + } + + return value; + } + + private static void WriteOptionalString(JsonWriter writer, string name, string? value) + { + if (value is null) + { + return; + } + + WriteProperty(writer, name, value, null); + } + + private static void WriteProperty(JsonWriter writer, string name, object? value, JsonSerializer? serializer) + { + writer.WritePropertyName(name); + if (serializer is null) + { + writer.WriteValue(value); + } + else + { + serializer.Serialize(writer, value); + } } } } @@ -502,6 +755,14 @@ public partial class DiagnosticClientCapabilities : DynamicCapability /// [Optional] public bool RelatedDocumentSupport { get; set; } + + /// + /// Whether the client supports the `Diagnostic.message` property being a `MarkupContent`. + /// + /// @since 3.18.0 + /// + [Optional] + public bool MarkupMessageSupport { get; set; } } /// diff --git a/src/Protocol/Features/Document/DocumentRangeFormattingFeature.cs b/src/Protocol/Features/Document/DocumentRangeFormattingFeature.cs index 3a9d1168b..caf3bb5d5 100644 --- a/src/Protocol/Features/Document/DocumentRangeFormattingFeature.cs +++ b/src/Protocol/Features/Document/DocumentRangeFormattingFeature.cs @@ -4,6 +4,7 @@ using OmniSharp.Extensions.LanguageServer.Protocol.Client; using OmniSharp.Extensions.LanguageServer.Protocol.Client.Capabilities; using OmniSharp.Extensions.LanguageServer.Protocol.Generation; +using OmniSharp.Extensions.LanguageServer.Protocol.Serialization; using OmniSharp.Extensions.LanguageServer.Protocol.Server.Capabilities; // ReSharper disable once CheckNamespace @@ -36,10 +37,42 @@ public partial record DocumentRangeFormattingParams : ITextDocumentIdentifierPar public FormattingOptions Options { get; init; } = null!; } + [Parallel] + [Method(TextDocumentNames.RangesFormatting, Direction.ClientToServer)] + [GenerateHandler("OmniSharp.Extensions.LanguageServer.Protocol.Document", Name = "DocumentRangesFormatting")] + [GenerateHandlerMethods] + [GenerateRequestMethods(typeof(ITextDocumentLanguageClient), typeof(ILanguageClient))] + [RegistrationOptions(typeof(DocumentRangeFormattingRegistrationOptions))] + [Capability(typeof(DocumentRangeFormattingCapability))] + public partial record DocumentRangesFormattingParams : ITextDocumentIdentifierParams, IRequest, IWorkDoneProgressParams + { + /// + /// The document to format. + /// + public TextDocumentIdentifier TextDocument { get; init; } = null!; + + /// + /// The ranges to format. + /// + public Container Ranges { get; init; } = null!; + + /// + /// The format options. + /// + public FormattingOptions Options { get; init; } = null!; + } + [GenerateRegistrationOptions(nameof(ServerCapabilities.DocumentRangeFormattingProvider))] [RegistrationName(TextDocumentNames.RangeFormatting)] public partial class DocumentRangeFormattingRegistrationOptions : ITextDocumentRegistrationOptions, IWorkDoneProgressOptions { + /// + /// Whether the server supports formatting multiple ranges at once. + /// + /// @since 3.18.0 + /// + [Optional] + public bool RangesSupport { get; set; } } } @@ -48,6 +81,13 @@ namespace Client.Capabilities [CapabilityKey(nameof(ClientCapabilities.TextDocument), nameof(TextDocumentClientCapabilities.RangeFormatting))] public partial class DocumentRangeFormattingCapability : DynamicCapability { + /// + /// Whether the client supports formatting multiple ranges at once. + /// + /// @since 3.18.0 + /// + [Optional] + public bool RangesSupport { get; set; } } } diff --git a/src/Protocol/Features/Document/FoldingRangeFeature.cs b/src/Protocol/Features/Document/FoldingRangeFeature.cs index cafe80829..6b18671a7 100644 --- a/src/Protocol/Features/Document/FoldingRangeFeature.cs +++ b/src/Protocol/Features/Document/FoldingRangeFeature.cs @@ -1,5 +1,6 @@ using System.Diagnostics; using System.Runtime.Serialization; +using MediatR; using Newtonsoft.Json; using Newtonsoft.Json.Converters; using OmniSharp.Extensions.JsonRpc; @@ -9,6 +10,7 @@ using OmniSharp.Extensions.LanguageServer.Protocol.Generation; using OmniSharp.Extensions.LanguageServer.Protocol.Models; using OmniSharp.Extensions.LanguageServer.Protocol.Serialization; +using OmniSharp.Extensions.LanguageServer.Protocol.Server; using OmniSharp.Extensions.LanguageServer.Protocol.Server.Capabilities; // ReSharper disable once CheckNamespace @@ -127,6 +129,14 @@ public readonly partial struct FoldingRangeKind public partial class FoldingRangeRegistrationOptions : ITextDocumentRegistrationOptions, IWorkDoneProgressOptions, IStaticRegistrationOptions { } + + [Parallel] + [Method(WorkspaceNames.FoldingRangeRefresh, Direction.ServerToClient)] + [GenerateHandler("OmniSharp.Extensions.LanguageServer.Protocol.Workspace")] + [GenerateHandlerMethods] + [GenerateRequestMethods(typeof(IWorkspaceLanguageServer), typeof(ILanguageServer))] + [Capability(typeof(FoldingRangeWorkspaceClientCapabilities))] + public partial record FoldingRangeRefreshParams : IRequest; } namespace Client.Capabilities @@ -167,6 +177,21 @@ public partial class FoldingRangeCapability : DynamicCapability public FoldingRangeCapabilityFoldingRange? FoldingRange { get; set; } } + /// + /// Client workspace capabilities specific to folding ranges. + /// + /// @since 3.18.0 + /// + [CapabilityKey(nameof(ClientCapabilities.Workspace), nameof(WorkspaceClientCapabilities.FoldingRange))] + public class FoldingRangeWorkspaceClientCapabilities : ICapability + { + /// + /// Whether the client implementation supports a refresh request sent from the server to the client. + /// + [Optional] + public bool RefreshSupport { get; set; } + } + public partial class FoldingRangeCapabilityFoldingRangeKind { /// diff --git a/src/Protocol/Features/Document/InlayHintFeature.cs b/src/Protocol/Features/Document/InlayHintFeature.cs index 02e6e4b8e..95c53ff9b 100644 --- a/src/Protocol/Features/Document/InlayHintFeature.cs +++ b/src/Protocol/Features/Document/InlayHintFeature.cs @@ -337,7 +337,7 @@ public class InlayHintCapabilityResolveSupport /// /// @since 3.17.0. /// - [CapabilityKey(nameof(ClientCapabilities.Workspace), nameof(WorkspaceClientCapabilities.SemanticTokens))] + [CapabilityKey(nameof(ClientCapabilities.Workspace), nameof(WorkspaceClientCapabilities.InlayHint))] public class InlayHintWorkspaceClientCapabilities : ICapability { /// diff --git a/src/Protocol/Features/Document/InlineCompletionFeature.cs b/src/Protocol/Features/Document/InlineCompletionFeature.cs new file mode 100644 index 000000000..64b597345 --- /dev/null +++ b/src/Protocol/Features/Document/InlineCompletionFeature.cs @@ -0,0 +1,246 @@ +using System.Collections; +using System.Reflection; +using MediatR; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using OmniSharp.Extensions.JsonRpc; +using OmniSharp.Extensions.JsonRpc.Generation; +using OmniSharp.Extensions.LanguageServer.Protocol.Client; +using OmniSharp.Extensions.LanguageServer.Protocol.Client.Capabilities; +using OmniSharp.Extensions.LanguageServer.Protocol.Document; +using OmniSharp.Extensions.LanguageServer.Protocol.Generation; +using OmniSharp.Extensions.LanguageServer.Protocol.Models; +using OmniSharp.Extensions.LanguageServer.Protocol.Serialization; +using OmniSharp.Extensions.LanguageServer.Protocol.Serialization.Converters; +using OmniSharp.Extensions.LanguageServer.Protocol.Server.Capabilities; + +// ReSharper disable once CheckNamespace +namespace OmniSharp.Extensions.LanguageServer.Protocol +{ + namespace Models + { + [Parallel] + [Method(TextDocumentNames.InlineCompletion, Direction.ClientToServer)] + [ + GenerateHandler("OmniSharp.Extensions.LanguageServer.Protocol.Document"), + GenerateHandlerMethods, + GenerateRequestMethods(typeof(ITextDocumentLanguageClient), typeof(ILanguageClient)) + ] + [RegistrationOptions(typeof(InlineCompletionRegistrationOptions)), Capability(typeof(InlineCompletionClientCapabilities))] + public partial record InlineCompletionParams : TextDocumentPositionParams, IWorkDoneProgressParams, + IRequest + { + /// + /// Additional information about the context in which inline completions were requested. + /// + public InlineCompletionContext Context { get; init; } = null!; + } + + /// + /// Provides information about the context in which an inline completion was requested. + /// + /// @since 3.18.0 + /// + public record InlineCompletionContext + { + /// + /// Describes how the inline completion was triggered. + /// + public InlineCompletionTriggerKind TriggerKind { get; init; } + + /// + /// Provides information about the currently selected item in the autocomplete widget if it is visible. + /// + [Optional] + public SelectedCompletionInfo? SelectedCompletionInfo { get; init; } + } + + /// + /// Describes the currently selected completion item. + /// + /// @since 3.18.0 + /// + public record SelectedCompletionInfo + { + /// + /// The range that will be replaced if this completion item is accepted. + /// + public Range Range { get; init; } = null!; + + /// + /// The text the range will be replaced with if this completion is accepted. + /// + public string Text { get; init; } = null!; + } + + /// + /// Describes how an inline completion was triggered. + /// + /// @since 3.18.0 + /// + [JsonConverter(typeof(NumberEnumConverter))] + public enum InlineCompletionTriggerKind + { + /// + /// Completion was triggered explicitly by a user gesture. + /// + Invoked = 1, + + /// + /// Completion was triggered automatically while editing. + /// + Automatic = 2 + } + + /// + /// An inline completion item represents text proposed inline to complete text that is being typed. + /// + /// @since 3.18.0 + /// + [GenerateContainer("InlineCompletionList", GenerateImplicitConversion = false)] + public partial record InlineCompletionItem + { + /// + /// The text to replace the range with. Must be set. + /// + public StringOrStringValue InsertText { get; init; } = null!; + + /// + /// A text that is used to decide if this inline completion should be shown. + /// + [Optional] + public string? FilterText { get; init; } + + /// + /// The range to replace. Must begin and end on the same line. + /// + [Optional] + public Range? Range { get; init; } + + /// + /// An optional command that is executed after inserting this completion. + /// + [Optional] + public Command? Command { get; init; } + } + + /// + /// Represents a collection of inline completion items to be presented in the editor. + /// + /// @since 3.18.0 + /// + [JsonConverter(typeof(Converter))] + public partial class InlineCompletionList + { + public IEnumerable Items => this; + + public static InlineCompletionList From(InlineCompletionList? source, IEnumerable? result) + => new((source?.Items ?? Array.Empty()).Concat(result ?? Array.Empty())); + + internal class Converter : JsonConverter + { + public override void WriteJson(JsonWriter writer, InlineCompletionList? value, JsonSerializer serializer) + { + writer.WriteStartObject(); + writer.WritePropertyName("items"); + serializer.Serialize(writer, (value?.Items ?? Array.Empty()).ToArray()); + writer.WriteEndObject(); + } + + public override InlineCompletionList? ReadJson( + JsonReader reader, Type objectType, InlineCompletionList? existingValue, bool hasExistingValue, JsonSerializer serializer + ) + { + if (reader.TokenType == JsonToken.StartArray) + { + return new InlineCompletionList(JArray.Load(reader).ToObject>(serializer)!); + } + + if (reader.TokenType == JsonToken.Null) + { + return null; + } + + var result = JObject.Load(reader); + return new InlineCompletionList(result["items"]!.ToObject>(serializer)!); + } + + public override bool CanRead => true; + } + } + + /// + /// A string value used as a snippet. + /// + /// @since 3.18.0 + /// + public record StringValue + { + public string Kind { get; init; } = "snippet"; + + public string Value { get; init; } = null!; + } + + [JsonConverter(typeof(Converter))] + public record StringOrStringValue + { + public StringOrStringValue(string value) => String = value; + + public StringOrStringValue(StringValue stringValue) => StringValue = stringValue; + + public string? String { get; } + public bool HasString => StringValue is null; + + public StringValue? StringValue { get; } + public bool HasStringValue => StringValue is { }; + + public static implicit operator StringOrStringValue?(string? value) => value is null ? null : new StringOrStringValue(value); + + public static implicit operator StringOrStringValue?(StringValue? value) => value is null ? null : new StringOrStringValue(value); + + internal class Converter : JsonConverter + { + public override void WriteJson(JsonWriter writer, StringOrStringValue value, JsonSerializer serializer) + { + if (value.HasString) + { + writer.WriteValue(value.String); + } + else + { + serializer.Serialize(writer, value.StringValue); + } + } + + public override StringOrStringValue ReadJson( + JsonReader reader, Type objectType, StringOrStringValue existingValue, bool hasExistingValue, JsonSerializer serializer + ) + { + return reader.TokenType == JsonToken.StartObject + ? new StringOrStringValue(JObject.Load(reader).ToObject(serializer)!) + : new StringOrStringValue((reader.Value as string)!); + } + + public override bool CanRead => true; + } + } + + [GenerateRegistrationOptions(nameof(ServerCapabilities.InlineCompletionProvider))] + [RegistrationName(TextDocumentNames.InlineCompletion)] + public partial class InlineCompletionRegistrationOptions : ITextDocumentRegistrationOptions, IWorkDoneProgressOptions, IStaticRegistrationOptions + { + } + } + + namespace Client.Capabilities + { + [CapabilityKey(nameof(ClientCapabilities.TextDocument), nameof(TextDocumentClientCapabilities.InlineCompletion))] + public partial class InlineCompletionClientCapabilities : DynamicCapability + { + } + } + + namespace Document + { + } +} diff --git a/src/Protocol/Features/Document/NotebookDocumentSyncFeature.cs b/src/Protocol/Features/Document/NotebookDocumentSyncFeature.cs index d759bd8f3..2902daebd 100644 --- a/src/Protocol/Features/Document/NotebookDocumentSyncFeature.cs +++ b/src/Protocol/Features/Document/NotebookDocumentSyncFeature.cs @@ -133,12 +133,14 @@ public partial record NotebookDocumentChangeEvent /// /// The changed meta data if any. /// + [Optional] public JObject? Metadata { get; init; } /// /// Changes to cells /// - public NotebookDocumentChangeEventCells Cells { get; init; } + [Optional] + public NotebookDocumentChangeEventCells? Cells { get; init; } } /// @@ -150,18 +152,21 @@ public partial record NotebookDocumentChangeEventCells /// Changes to the cell structure to add or /// remove cells. /// + [Optional] public NotebookDocumentChangeEventCellsStructure? Structure { get; init; } /// /// Changes to notebook cells properties like its /// kind, execution summary or metadata. /// + [Optional] public Container? Data { get; set; } /// /// Changes to the text content of notebook cells. /// - public NotebookDocumentChangeEventCellsTextContent TextContent { get; init; } + [Optional] + public Container? TextContent { get; init; } } /// @@ -178,11 +183,13 @@ public partial record NotebookDocumentChangeEventCellsStructure /// /// Additional opened cell text documents. /// + [Optional] public Container? DidOpen { get; set; } /// /// Additional closed cell text documents. /// + [Optional] public Container? DidClose { get; set; } } @@ -207,6 +214,7 @@ public partial record NotebookCellArrayChange /// /// The new cells, if any /// + [Optional] public Container? Cells { get; set; } } @@ -273,6 +281,7 @@ public partial record NotebookDocument /// Additional metadata stored with the notebook /// document. /// + [Optional] public JObject? Metadata { get; set; } /// @@ -307,12 +316,14 @@ public partial record NotebookCell /// /// Additional metadata stored with the cell. /// + [Optional] public JObject? Metadata { get; set; } /// /// Additional execution summary information /// if supported by the client. /// + [Optional] public ExecutionSummary? ExecutionSummary { get; set; } } @@ -329,6 +340,7 @@ public partial record ExecutionSummary /// Whether the execution was successful or /// not if known by the client. /// + [Optional] public bool? Success { get; set; } } @@ -455,13 +467,13 @@ public class NotebookDocumentFilter : IEquatable /// A glob pattern, like `*.{ts,js}`. /// [Optional] - public string? Pattern + public GlobPattern? Pattern { get => _pattern; init { _pattern = value; - _minimatcher = new Minimatcher(value!, new Options { MatchBase = true }); + _minimatcher = new Minimatcher(GetPattern(value)!, new Options { MatchBase = true }); } } @@ -471,9 +483,12 @@ public string? Pattern [JsonIgnore] public bool HasPattern => Pattern != null; - private string? _pattern; + private GlobPattern? _pattern; private Minimatcher? _minimatcher; + private static string? GetPattern(GlobPattern? pattern) => + pattern?.HasRelativePattern == true ? pattern.RelativePattern?.Pattern : pattern?.Pattern; + public static explicit operator string(NotebookDocumentFilter notebookDocumentFilter) { var items = new List(); @@ -489,7 +504,7 @@ public static explicit operator string(NotebookDocumentFilter notebookDocumentFi if (notebookDocumentFilter.HasPattern) { - items.Add(notebookDocumentFilter.Pattern!); + items.Add(notebookDocumentFilter.Pattern!.ToString()); } return $"[{string.Join(", ", items)}]"; @@ -539,7 +554,7 @@ public bool Equals(NotebookDocumentFilter? other) { if (ReferenceEquals(null, other)) return false; if (ReferenceEquals(this, other)) return true; - return _pattern == other._pattern && NotebookType == other.NotebookType && Scheme == other.Scheme; + return Equals(_pattern, other._pattern) && NotebookType == other.NotebookType && Scheme == other.Scheme; } public override bool Equals(object? obj) diff --git a/src/Protocol/Features/Document/SignatureHelpFeature.cs b/src/Protocol/Features/Document/SignatureHelpFeature.cs index 9076ecba5..5282248d8 100644 --- a/src/Protocol/Features/Document/SignatureHelpFeature.cs +++ b/src/Protocol/Features/Document/SignatureHelpFeature.cs @@ -299,6 +299,14 @@ public class SignatureInformationCapabilityOptions /// [Optional] public bool ActiveParameterSupport { get; set; } + + /// + /// The client supports the `activeParameter` property being `null`. + /// + /// @since 3.18.0 + /// + [Optional] + public bool NoActiveParameterSupport { get; set; } } public class SignatureParameterInformationCapabilityOptions diff --git a/src/Protocol/Features/Document/TextDocumentSyncFeature.cs b/src/Protocol/Features/Document/TextDocumentSyncFeature.cs index ce06f1a1c..374602367 100644 --- a/src/Protocol/Features/Document/TextDocumentSyncFeature.cs +++ b/src/Protocol/Features/Document/TextDocumentSyncFeature.cs @@ -225,13 +225,13 @@ public class TextDocumentFilter : IEquatable /// not `example.0`) /// [Optional] - public string? Pattern + public GlobPattern? Pattern { get => _pattern; init { _pattern = value; - _minimatcher = new Minimatcher(value!, new Options { MatchBase = true }); + _minimatcher = new Minimatcher(GetPattern(value)!, new Options { MatchBase = true }); } } @@ -241,9 +241,12 @@ public string? Pattern [JsonIgnore] public bool HasPattern => Pattern != null; - private string? _pattern; + private GlobPattern? _pattern; private Minimatcher? _minimatcher; + private static string? GetPattern(GlobPattern? pattern) => + pattern?.HasRelativePattern == true ? pattern.RelativePattern?.Pattern : pattern?.Pattern; + public static explicit operator string(TextDocumentFilter textDocumentFilter) { var items = new List(); @@ -259,7 +262,7 @@ public static explicit operator string(TextDocumentFilter textDocumentFilter) if (textDocumentFilter.HasPattern) { - items.Add(textDocumentFilter.Pattern!); + items.Add(textDocumentFilter.Pattern!.ToString()); } return $"[{string.Join(", ", items)}]"; @@ -309,7 +312,7 @@ public bool Equals(TextDocumentFilter? other) { if (ReferenceEquals(null, other)) return false; if (ReferenceEquals(this, other)) return true; - return _pattern == other._pattern && Language == other.Language && Scheme == other.Scheme; + return Equals(_pattern, other._pattern) && Language == other.Language && Scheme == other.Scheme; } public override bool Equals(object? obj) @@ -588,6 +591,20 @@ public class TextSynchronizationCapability : DynamicCapability [Optional] public bool DidSave { get; set; } } + + /// + /// Client capabilities specific to text document filters. + /// + /// @since 3.18.0 + /// + public class TextDocumentFilterClientCapabilities + { + /// + /// The client supports relative patterns in text document filters. + /// + [Optional] + public bool RelativePatternSupport { get; set; } + } } namespace Document @@ -1036,6 +1053,7 @@ public override TextDocumentAttributes GetTextDocumentAttributes(DocumentUri uri { return _getTextDocumentAttributes.Invoke(uri); } + } } } diff --git a/src/Protocol/Features/Document/TypeHierarchyFeature.cs b/src/Protocol/Features/Document/TypeHierarchyFeature.cs index 97a5eb640..c9e0a7606 100644 --- a/src/Protocol/Features/Document/TypeHierarchyFeature.cs +++ b/src/Protocol/Features/Document/TypeHierarchyFeature.cs @@ -182,7 +182,7 @@ public static TypeHierarchySubtypesParams Create(TypeHierarchySubtypesPara { Item = item.Item, PartialResultToken = item.PartialResultToken, - WorkDoneToken = item.PartialResultToken + WorkDoneToken = item.WorkDoneToken }; } } @@ -204,7 +204,7 @@ public static TypeHierarchySubtypesParams Create(TypeHierarchySubtypesParams { Item = item.Item, PartialResultToken = item.PartialResultToken, - WorkDoneToken = item.PartialResultToken + WorkDoneToken = item.WorkDoneToken }; } diff --git a/src/Protocol/Features/Workspace/ApplyEditFeature.cs b/src/Protocol/Features/Workspace/ApplyEditFeature.cs index 62fadc7ab..10a7f772d 100644 --- a/src/Protocol/Features/Workspace/ApplyEditFeature.cs +++ b/src/Protocol/Features/Workspace/ApplyEditFeature.cs @@ -26,6 +26,14 @@ public partial record ApplyWorkspaceEditParams : IRequest public WorkspaceEdit Edit { get; init; } + + /// + /// Additional data about the workspace edit. + /// + /// @since 3.18.0 + /// + [Optional] + public WorkspaceEditMetadata? Metadata { get; init; } } public partial record ApplyWorkspaceEditResponse diff --git a/src/Protocol/Features/Workspace/TextDocumentContentFeature.cs b/src/Protocol/Features/Workspace/TextDocumentContentFeature.cs new file mode 100644 index 000000000..10ab27796 --- /dev/null +++ b/src/Protocol/Features/Workspace/TextDocumentContentFeature.cs @@ -0,0 +1,76 @@ +using MediatR; +using OmniSharp.Extensions.JsonRpc; +using OmniSharp.Extensions.JsonRpc.Generation; +using OmniSharp.Extensions.LanguageServer.Protocol.Client; +using OmniSharp.Extensions.LanguageServer.Protocol.Client.Capabilities; +using OmniSharp.Extensions.LanguageServer.Protocol.Generation; +using OmniSharp.Extensions.LanguageServer.Protocol.Models; +using OmniSharp.Extensions.LanguageServer.Protocol.Serialization; +using OmniSharp.Extensions.LanguageServer.Protocol.Server; +using OmniSharp.Extensions.LanguageServer.Protocol.Server.Capabilities; + +// ReSharper disable once CheckNamespace +namespace OmniSharp.Extensions.LanguageServer.Protocol +{ + namespace Models + { + [Parallel] + [Method(WorkspaceNames.TextDocumentContent, Direction.ClientToServer)] + [GenerateHandler("OmniSharp.Extensions.LanguageServer.Protocol.Workspace")] + [GenerateHandlerMethods] + [GenerateRequestMethods(typeof(IWorkspaceLanguageClient), typeof(ILanguageClient))] + [RegistrationOptions(typeof(TextDocumentContentRegistrationOptions))] + [Capability(typeof(TextDocumentContentClientCapabilities))] + public partial record TextDocumentContentParams : IRequest + { + /// + /// The URI of the text document. + /// + public DocumentUri Uri { get; init; } + } + + /// + /// Result of the `workspace/textDocumentContent` request. + /// + /// @since 3.18.0 + /// + public partial record TextDocumentContentResult + { + /// + /// The text content of the text document. + /// + public string Text { get; init; } = null!; + } + + [Parallel] + [Method(WorkspaceNames.TextDocumentContentRefresh, Direction.ServerToClient)] + [GenerateHandler("OmniSharp.Extensions.LanguageServer.Protocol.Workspace")] + [GenerateHandlerMethods] + [GenerateRequestMethods(typeof(IWorkspaceLanguageServer), typeof(ILanguageServer))] + public partial record TextDocumentContentRefreshParams : IRequest + { + /// + /// The URI of the text document to refresh. + /// + public DocumentUri Uri { get; init; } + } + + [GenerateRegistrationOptions(nameof(ServerCapabilities.Workspace), nameof(WorkspaceServerCapabilities.TextDocumentContent))] + [RegistrationName(WorkspaceNames.TextDocumentContent)] + public partial class TextDocumentContentRegistrationOptions : IStaticRegistrationOptions + { + /// + /// The schemes for which the server provides content. + /// + public Container Schemes { get; set; } = new(); + } + } + + namespace Client.Capabilities + { + [CapabilityKey(nameof(ClientCapabilities.Workspace), nameof(WorkspaceClientCapabilities.TextDocumentContent))] + public partial class TextDocumentContentClientCapabilities : DynamicCapability + { + } + } +} diff --git a/src/Protocol/LanguageProtocolProxy.cs b/src/Protocol/LanguageProtocolProxy.cs index f81be7c8b..2da1f7896 100644 --- a/src/Protocol/LanguageProtocolProxy.cs +++ b/src/Protocol/LanguageProtocolProxy.cs @@ -63,7 +63,7 @@ public Task SendRequest(IRequest request, Cance return _responseRouter.SendRequest(request, cancellationToken); } - bool IResponseRouter.TryGetRequest(long id, [NotNullWhen(true)] out string? method, [NotNullWhen(true)] out TaskCompletionSource? pendingTask) + bool IResponseRouter.TryGetRequest(object id, [NotNullWhen(true)] out string? method, [NotNullWhen(true)] out TaskCompletionSource? pendingTask) { return _responseRouter.TryGetRequest(id, out method, out pendingTask); } diff --git a/src/Protocol/Models/Command.cs b/src/Protocol/Models/Command.cs index b21328eb5..4cac5a7ae 100644 --- a/src/Protocol/Models/Command.cs +++ b/src/Protocol/Models/Command.cs @@ -20,6 +20,14 @@ public record Command [JsonProperty("command")] public string Name { get; init; } = null!; + /// + /// An optional tooltip. + /// + /// @since 3.18.0 + /// + [Optional] + public string? Tooltip { get; init; } + /// /// Arguments that the command handler should be /// invoked with. diff --git a/src/Protocol/Models/Diagnostic.cs b/src/Protocol/Models/Diagnostic.cs index b61a463ff..5e6677206 100644 --- a/src/Protocol/Models/Diagnostic.cs +++ b/src/Protocol/Models/Diagnostic.cs @@ -49,7 +49,7 @@ public partial record Diagnostic : ICanHaveData /// /// The diagnostic's message. /// - public string Message { get; init; } = null!; + public StringOrMarkupContent Message { get; init; } = null!; /// /// Additional metadata about the diagnostic. @@ -81,7 +81,7 @@ public partial record Diagnostic : ICanHaveData $"{Range}" + $"{( string.IsNullOrWhiteSpace(Source) ? "" : $" ({Source})" )}" + $"{( Tags?.Any() == true ? $" [tags: {string.Join(", ", Tags.Select(z => z.ToString()))}]" : "" )}" + - $" {( Message?.Length > 20 ? Message.Substring(0, 20) : Message )}"; + $" {( Message?.ToString()?.Length > 20 ? Message.ToString()!.Substring(0, 20) : Message )}"; } [JsonConverter(typeof(DiagnosticCodeConverter))] diff --git a/src/Protocol/Models/LSPAny.cs b/src/Protocol/Models/LSPAny.cs new file mode 100644 index 000000000..ba284ddcb --- /dev/null +++ b/src/Protocol/Models/LSPAny.cs @@ -0,0 +1,99 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace OmniSharp.Extensions.LanguageServer.Protocol.Models +{ + /// + /// The LSP any type. + /// + /// @since 3.17.0 + /// + [JsonConverter(typeof(Converter))] + public readonly partial struct LSPAny : IEquatable + { + public LSPAny(JToken? value) + { + Value = value; + } + + public JToken? Value { get; } + + public static LSPAny From(object? value) => + value switch + { + null => new LSPAny(JValue.CreateNull()), + LSPAny any => any, + JToken token => new LSPAny(token), + _ => new LSPAny(JToken.FromObject(value)) + }; + + public bool Equals(LSPAny other) => JToken.DeepEquals(Value, other.Value); + + public override bool Equals(object? obj) => obj is LSPAny other && Equals(other); + + public override int GetHashCode() => Value?.GetHashCode() ?? 0; + + public override string ToString() => Value?.ToString(Formatting.None) ?? "null"; + + public static implicit operator LSPAny(JToken? value) => new(value); + + public static implicit operator JToken?(LSPAny value) => value.Value; + + public static bool operator ==(LSPAny left, LSPAny right) => left.Equals(right); + + public static bool operator !=(LSPAny left, LSPAny right) => !left.Equals(right); + + internal class Converter : JsonConverter + { + public override void WriteJson(JsonWriter writer, LSPAny value, JsonSerializer serializer) + { + if (value.Value is null) + { + writer.WriteNull(); + return; + } + + value.Value.WriteTo(writer); + } + + public override LSPAny ReadJson(JsonReader reader, Type objectType, LSPAny existingValue, bool hasExistingValue, JsonSerializer serializer) + { + return reader.TokenType == JsonToken.Null + ? new LSPAny(JValue.CreateNull()) + : new LSPAny(JToken.ReadFrom(reader)); + } + } + } + + /// + /// LSP object definition. + /// + /// @since 3.17.0 + /// + public partial class LSPObject : JObject + { + public LSPObject() + { + } + + public LSPObject(params object[] content) : base(content) + { + } + } + + /// + /// LSP arrays. + /// + /// @since 3.17.0 + /// + public partial class LSPArray : JArray + { + public LSPArray() + { + } + + public LSPArray(params object[] content) : base(content) + { + } + } +} diff --git a/src/Protocol/Models/MessageType.cs b/src/Protocol/Models/MessageType.cs index dd2a8aadc..eb702a16e 100644 --- a/src/Protocol/Models/MessageType.cs +++ b/src/Protocol/Models/MessageType.cs @@ -25,5 +25,12 @@ public enum MessageType /// A log message. /// Log = 4, + + /// + /// A debug message. + /// + /// @since 3.18.0 + /// + Debug = 5, } } diff --git a/src/Protocol/Models/TextEdit.cs b/src/Protocol/Models/TextEdit.cs index e736b5cd0..f99ce5b3d 100644 --- a/src/Protocol/Models/TextEdit.cs +++ b/src/Protocol/Models/TextEdit.cs @@ -66,6 +66,36 @@ public override string ToString() } } + /// + /// An interactive text edit. + /// + /// @since 3.18.0 + /// + [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] + [JsonConverter(typeof(TextEditConverter))] + public record SnippetTextEdit : TextEdit + { + /// + /// The snippet to be inserted. + /// + public StringValue Snippet { get; init; } = null!; + + /// + /// The actual identifier of the snippet edit. + /// + [Optional] + public ChangeAnnotationIdentifier? AnnotationId { get; init; } + + private string DebuggerDisplay => + $"snippet: {Range} {( string.IsNullOrWhiteSpace(Snippet?.Value) ? string.Empty : Snippet.Value.Length > 30 ? Snippet.Value.Substring(0, 30) : Snippet.Value )}"; + + /// + public override string ToString() + { + return DebuggerDisplay; + } + } + [JsonConverter(typeof(TextEditOrInsertReplaceEditConverter))] [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] [GenerateContainer] diff --git a/src/Protocol/Models/WorkspaceEdit.cs b/src/Protocol/Models/WorkspaceEdit.cs index a65cc41ec..52d6c50c3 100644 --- a/src/Protocol/Models/WorkspaceEdit.cs +++ b/src/Protocol/Models/WorkspaceEdit.cs @@ -41,5 +41,27 @@ public record WorkspaceEdit /// [Optional] public IDictionary? ChangeAnnotations { get; init; } + + /// + /// Additional data about the workspace edit. + /// + /// @since 3.18.0 + /// + [Optional] + public WorkspaceEditMetadata? Metadata { get; init; } + } + + /// + /// Additional data about a workspace edit. + /// + /// @since 3.18.0 + /// + public record WorkspaceEditMetadata + { + /// + /// Signal to the editor that this edit is a refactoring. + /// + [Optional] + public bool IsRefactoring { get; init; } } } diff --git a/src/Protocol/Protocol.csproj b/src/Protocol/Protocol.csproj index 643f3c301..6e42fcde3 100644 --- a/src/Protocol/Protocol.csproj +++ b/src/Protocol/Protocol.csproj @@ -1,6 +1,6 @@ - netstandard2.1;netstandard2.0;net6.0;net8.0 + $(LibraryTargetFrameworks) AnyCPU OmniSharp.Extensions.LanguageProtocol OmniSharp.Extensions.LanguageServer.Protocol diff --git a/src/Protocol/Serialization/Converters/TextEditConverter.cs b/src/Protocol/Serialization/Converters/TextEditConverter.cs index 5c180881a..6a9f72853 100644 --- a/src/Protocol/Serialization/Converters/TextEditConverter.cs +++ b/src/Protocol/Serialization/Converters/TextEditConverter.cs @@ -13,6 +13,20 @@ public override void WriteJson(JsonWriter writer, TextEdit value, JsonSerializer writer.WriteStartObject(); writer.WritePropertyName("range"); serializer.Serialize(writer, value.Range); + if (value is SnippetTextEdit snippetTextEdit) + { + writer.WritePropertyName("snippet"); + serializer.Serialize(writer, snippetTextEdit.Snippet); + if (snippetTextEdit.AnnotationId is { }) + { + writer.WritePropertyName("annotationId"); + serializer.Serialize(writer, snippetTextEdit.AnnotationId); + } + + writer.WriteEndObject(); + return; + } + writer.WritePropertyName("newText"); serializer.Serialize(writer, value.NewText); if (value is AnnotatedTextEdit annotatedTextEdit) @@ -28,7 +42,14 @@ public override TextEdit ReadJson(JsonReader reader, Type objectType, TextEdit e { var result = JObject.Load(reader); TextEdit edit; - if (result["annotationId"] is { Type: JTokenType.String } annotation) + if (result["snippet"] is { Type: JTokenType.Object } snippet) + { + edit = new SnippetTextEdit() { + Snippet = snippet.ToObject(serializer)!, + AnnotationId = result["annotationId"]?.ToObject(serializer) + }; + } + else if (result["annotationId"] is { Type: JTokenType.String } annotation) { edit = new AnnotatedTextEdit() { AnnotationId = annotation.ToObject(serializer) diff --git a/src/Protocol/Server/Capabilities/IServerCapabilities.cs b/src/Protocol/Server/Capabilities/IServerCapabilities.cs index d410f321c..80d943122 100644 --- a/src/Protocol/Server/Capabilities/IServerCapabilities.cs +++ b/src/Protocol/Server/Capabilities/IServerCapabilities.cs @@ -32,12 +32,12 @@ public interface IServerCapabilities : ICapabilitiesBase /// TextDocumentSync? TextDocumentSync { get; set; } -// /// -// /// Defines how notebook documents are synced. -// /// -// /// @since 3.17.0 -// /// -// NotebookDocumentSyncOptions.StaticOptions NotebookDocumentSync { get; set; } + /// + /// Defines how notebook documents are synced. + /// + /// @since 3.17.0 + /// + NotebookDocumentSyncOptions? NotebookDocumentSync { get; set; } /// /// The server provides hover support. @@ -215,5 +215,12 @@ public interface IServerCapabilities : ICapabilitiesBase /// @since 3.17.0 /// DiagnosticsRegistrationOptions.StaticOptions? DiagnosticProvider { get; set; } + + /// + /// The server provides inline completions. + /// + /// @since 3.18.0 + /// + BooleanOr? InlineCompletionProvider { get; set; } } } diff --git a/src/Protocol/Server/Capabilities/IWorkspaceServerCapabilities.cs b/src/Protocol/Server/Capabilities/IWorkspaceServerCapabilities.cs index 276e1c510..7b290e1db 100644 --- a/src/Protocol/Server/Capabilities/IWorkspaceServerCapabilities.cs +++ b/src/Protocol/Server/Capabilities/IWorkspaceServerCapabilities.cs @@ -17,5 +17,12 @@ public interface IWorkspaceServerCapabilities : ICapabilitiesBase /// @since 3.16.0 /// FileOperationsWorkspaceServerCapabilities? FileOperations { get; set; } + + /// + /// The server provides dynamic text document content. + /// + /// @since 3.18.0 + /// + TextDocumentContentRegistrationOptions.StaticOptions? TextDocumentContent { get; set; } } } diff --git a/src/Protocol/Server/Capabilities/ServerCapabilities.cs b/src/Protocol/Server/Capabilities/ServerCapabilities.cs index 6d2c3e9ce..334a3e5a4 100644 --- a/src/Protocol/Server/Capabilities/ServerCapabilities.cs +++ b/src/Protocol/Server/Capabilities/ServerCapabilities.cs @@ -250,6 +250,14 @@ public class ServerCapabilities : CapabilitiesBase, IServerCapabilities [Optional] public DiagnosticsRegistrationOptions.StaticOptions? DiagnosticProvider { get; set; } + /// + /// The server provides inline completions. + /// + /// @since 3.18.0 + /// + [Optional] + public BooleanOr? InlineCompletionProvider { get; set; } + /// /// Workspace specific server capabilities /// diff --git a/src/Protocol/Server/Capabilities/WorkspaceServerCapabilities.cs b/src/Protocol/Server/Capabilities/WorkspaceServerCapabilities.cs index 2e62af17c..95cbb1951 100644 --- a/src/Protocol/Server/Capabilities/WorkspaceServerCapabilities.cs +++ b/src/Protocol/Server/Capabilities/WorkspaceServerCapabilities.cs @@ -20,5 +20,13 @@ public class WorkspaceServerCapabilities : CapabilitiesBase, IWorkspaceServerCap /// [Optional] public FileOperationsWorkspaceServerCapabilities? FileOperations { get; set; } + + /// + /// The server provides dynamic text document content. + /// + /// @since 3.18.0 + /// + [Optional] + public TextDocumentContentRegistrationOptions.StaticOptions? TextDocumentContent { get; set; } } } diff --git a/src/Protocol/TextDocumentNames.cs b/src/Protocol/TextDocumentNames.cs index 2e99158eb..2bf2cb182 100644 --- a/src/Protocol/TextDocumentNames.cs +++ b/src/Protocol/TextDocumentNames.cs @@ -22,6 +22,7 @@ public static class TextDocumentNames public const string DocumentLinkResolve = "documentLink/resolve"; public const string OnTypeFormatting = "textDocument/onTypeFormatting"; public const string RangeFormatting = "textDocument/rangeFormatting"; + public const string RangesFormatting = "textDocument/rangesFormatting"; public const string DocumentSymbol = "textDocument/documentSymbol"; public const string Hover = "textDocument/hover"; public const string Implementation = "textDocument/implementation"; @@ -47,6 +48,7 @@ public static class TextDocumentNames public const string InlayHint = "textDocument/inlayHint"; public const string InlayHintResolve = "inlayHint/resolve"; public const string InlineValue = "textDocument/inlineValue"; + public const string InlineCompletion = "textDocument/inlineCompletion"; public const string Diagnostics = "textDocument/diagnostic"; public const string PrepareTypeHierarchy = "textDocument/prepareTypeHierarchy"; public const string TypeHierarchySupertypes = "typeHierarchy/supertypes"; diff --git a/src/Protocol/WorkspaceNames.cs b/src/Protocol/WorkspaceNames.cs index 4c757b513..2292c698e 100644 --- a/src/Protocol/WorkspaceNames.cs +++ b/src/Protocol/WorkspaceNames.cs @@ -13,6 +13,7 @@ public static class WorkspaceNames public const string WorkspaceFolders = "workspace/workspaceFolders"; public const string SemanticTokensRefresh = "workspace/semanticTokens/refresh"; public const string CodeLensRefresh = "workspace/codeLens/refresh"; + public const string FoldingRangeRefresh = "workspace/foldingRange/refresh"; public const string WillCreateFiles = "workspace/willCreateFiles"; public const string DidCreateFiles = "workspace/didCreateFiles"; public const string WillRenameFiles = "workspace/willRenameFiles"; @@ -23,5 +24,7 @@ public static class WorkspaceNames public const string DiagnosticRefresh = "workspace/diagnostic/refresh"; public const string Diagnostics = "workspace/diagnostic"; public const string InlayHintRefresh = "workspace/inlayHint/refresh"; + public const string TextDocumentContent = "workspace/textDocumentContent"; + public const string TextDocumentContentRefresh = "workspace/textDocumentContent/refresh"; } } diff --git a/src/Server/Matchers/NotebookDocumentMatcher.cs b/src/Server/Matchers/NotebookDocumentMatcher.cs index 900831963..5442ed31e 100644 --- a/src/Server/Matchers/NotebookDocumentMatcher.cs +++ b/src/Server/Matchers/NotebookDocumentMatcher.cs @@ -62,12 +62,12 @@ public IEnumerable FindHandler(object parameters, IEnumer var attributes = GetNotebookDocumentAttributes(didChangeDocumentParams.NotebookDocument.Uri); attributes.AddRange( - didChangeDocumentParams.Change.Cells.Structure?.Array.Cells? + didChangeDocumentParams.Change.Cells?.Structure?.Array.Cells? .Select(z => new NotebookDocumentAttributes(z.Document)) .Distinct() ?? Array.Empty() ); attributes.AddRange( - didChangeDocumentParams.Change.Cells.Data? + didChangeDocumentParams.Change.Cells?.Data? .Select(z => new NotebookDocumentAttributes(z.Document)) .Distinct() ?? Array.Empty() ); diff --git a/src/Server/Server.csproj b/src/Server/Server.csproj index f48ab4a52..6da64c207 100644 --- a/src/Server/Server.csproj +++ b/src/Server/Server.csproj @@ -1,6 +1,6 @@  - netstandard2.1;netstandard2.0;net6.0;net8.0 + $(LibraryTargetFrameworks) AnyCPU OmniSharp.Extensions.LanguageServer OmniSharp.Extensions.LanguageServer.Server diff --git a/src/Shared/Shared.csproj b/src/Shared/Shared.csproj index c7d7d66ca..5b66cb12d 100644 --- a/src/Shared/Shared.csproj +++ b/src/Shared/Shared.csproj @@ -1,6 +1,6 @@ - netstandard2.1;netstandard2.0;net6.0;net8.0 + $(LibraryTargetFrameworks) AnyCPU OmniSharp.Extensions.LanguageServer.Shared OmniSharp.Extensions.LanguageServer.Shared diff --git a/src/Testing/Testing.csproj b/src/Testing/Testing.csproj index d1964f10d..e23a4d676 100644 --- a/src/Testing/Testing.csproj +++ b/src/Testing/Testing.csproj @@ -1,7 +1,7 @@  - netstandard2.1;netstandard2.0;net6.0;net8.0 + $(LibraryTargetFrameworks) AnyCPU OmniSharp.Extensions.LanguageProtocol.Testing OmniSharp.Extensions.LanguageProtocol.Testing diff --git a/test/Client.Tests/Client.Tests.csproj b/test/Client.Tests/Client.Tests.csproj index 384c2bfaf..f1e878dc7 100644 --- a/test/Client.Tests/Client.Tests.csproj +++ b/test/Client.Tests/Client.Tests.csproj @@ -1,7 +1,7 @@ - net8.0 + $(TestTargetFrameworks) OmniSharp.Extensions.LanguageClient.Tests OmniSharp.Extensions.LanguageServer.Client.Tests diff --git a/test/Dap.Tests/Dap.Tests.csproj b/test/Dap.Tests/Dap.Tests.csproj index b3f146430..cf80b7601 100644 --- a/test/Dap.Tests/Dap.Tests.csproj +++ b/test/Dap.Tests/Dap.Tests.csproj @@ -1,6 +1,6 @@  - net8.0 + $(TestTargetFrameworks) true AnyCPU diff --git a/test/Generation.Tests/Generation.Tests.csproj b/test/Generation.Tests/Generation.Tests.csproj index 19309997c..4c1a31887 100644 --- a/test/Generation.Tests/Generation.Tests.csproj +++ b/test/Generation.Tests/Generation.Tests.csproj @@ -1,6 +1,6 @@ - net8.0 + $(TestTargetFrameworks) true AnyCPU diff --git a/test/JsonRpc.Tests/InputHandlerTests.cs b/test/JsonRpc.Tests/InputHandlerTests.cs index ba93492d7..d70cb7874 100644 --- a/test/JsonRpc.Tests/InputHandlerTests.cs +++ b/test/JsonRpc.Tests/InputHandlerTests.cs @@ -15,6 +15,7 @@ using OmniSharp.Extensions.JsonRpc.Client; using OmniSharp.Extensions.JsonRpc.Serialization; using OmniSharp.Extensions.JsonRpc.Server; +using OmniSharp.Extensions.JsonRpc.Server.Messages; using Xunit; using Xunit.Abstractions; @@ -115,6 +116,8 @@ await pipe.Writer.WriteAsync( [InlineData( "Content-Length: 2 \r\nContent-Type: application/json\r\n\r\n{}" )] + [InlineData("Content-Length: 2\r\nContent-Type: application/vscode-jsonrpc; charset=utf-8\r\n\r\n{}")] + [InlineData("Content-Length: 2\r\nContent-Type: application/vscode-jsonrpc; charset=utf8\r\n\r\n{}")] [InlineData("Content-Type: application/json\r\nContent-Length: 2\r\n\r\n{}")] [InlineData("Content-Type: application/json\r\nNot-A-Header: really\r\nContent-Length: 2\r\n\r\n{}")] [InlineData( @@ -150,6 +153,35 @@ public async Task Should_Handle_Different_Additional_Headers_and_Whitespace(stri receiver.Received(1).IsValid(Arg.Is(x => x.ToString() == "{}")); } + [Fact] + public async Task Should_Reject_Unsupported_Content_Type_Charset() + { + var pipe = new Pipe(new PipeOptions()); + + var outputHandler = Substitute.For(); + var receiver = Substitute.For(); + + using var handler = NewHandler( + pipe.Reader, outputHandler, receiver, + Substitute.For>(), + _loggerFactory, Substitute.For(), + Substitute.For() + ); + await pipe.Writer.WriteAsync( + Encoding.UTF8.GetBytes("Content-Type: application/vscode-jsonrpc; charset=iso-8859-1\r\nContent-Length: 2\r\n\r\n{}") + ); + + var cts = new CancellationTokenSource(); + cts.CancelAfter(TimeSpan.FromSeconds(5)); + var processTask = handler.ProcessInputStream(cts.Token); + + await pipe.Writer.CompleteAsync(); + await processTask; + + receiver.DidNotReceiveWithAnyArgs().IsValid(default!); + outputHandler.Received().Send(Arg.Is(x => x.Error!.Code == ErrorCodes.ParseError)); + } + [Fact] public async Task Should_Handle_Multiple_Requests_Back_To_Back() { @@ -259,6 +291,132 @@ public async Task Should_Handle_Multiple_Chunked_Requests(string content) receiver.Received(1).IsValid(Arg.Any()); } + [Fact] + public async Task Should_Complete_Pending_Request_With_String_Response_Id() + { + var pipe = new Pipe(new PipeOptions()); + + var outputHandler = Substitute.For(); + var receiver = new Receiver(); + var responseRouter = new ResponseRouter( + new Lazy(() => outputHandler), + new JsonRpcSerializer(), + new AssemblyScanningHandlerTypeDescriptorProvider(new[] { typeof(AssemblyScanningHandlerTypeDescriptorProvider).Assembly, typeof(InputHandlerTests).Assembly }) + ); + var pending = new TaskCompletionSource(); + responseRouter.Requests.TryAdd("request-id", ("method", pending)).Should().BeTrue(); + + using var handler = NewHandler( + pipe.Reader, outputHandler, receiver, + Substitute.For>(), + _loggerFactory, responseRouter, + Substitute.For() + ); + var content = @"{""jsonrpc"":""2.0"",""id"":""request-id"",""result"":{}}"; + handler.Start(); + await pipe.Writer.WriteAsync(Encoding.UTF8.GetBytes($"Content-Length: {Encoding.UTF8.GetByteCount(content)}\r\n\r\n{content}")); + + await pipe.Writer.CompleteAsync(); + await handler.InputCompleted; + + pending.Task.IsCompletedSuccessfully.Should().BeTrue(); + } + + [Theory] + [InlineData(ErrorCodes.RequestFailed, typeof(RequestFailedException))] + [InlineData(ErrorCodes.ServerCancelled, typeof(ServerCancelledException))] + [InlineData(ErrorCodes.UnknownErrorCode, typeof(UnknownErrorException))] + public async Task Should_Map_Response_Error_Codes_To_Lsp_Exceptions(int errorCode, Type exceptionType) + { + var pipe = new Pipe(new PipeOptions()); + + var outputHandler = Substitute.For(); + var receiver = new Receiver(); + var responseRouter = new ResponseRouter( + new Lazy(() => outputHandler), + new JsonRpcSerializer(), + new AssemblyScanningHandlerTypeDescriptorProvider(new[] { typeof(AssemblyScanningHandlerTypeDescriptorProvider).Assembly, typeof(InputHandlerTests).Assembly }) + ); + var pending = new TaskCompletionSource(); + responseRouter.Requests.TryAdd("request-id", ("method", pending)).Should().BeTrue(); + + using var handler = NewHandler( + pipe.Reader, outputHandler, receiver, + Substitute.For>(), + _loggerFactory, responseRouter, + Substitute.For() + ); + var content = $@"{{""jsonrpc"":""2.0"",""id"":""request-id"",""error"":{{""code"":{errorCode},""message"":""error""}}}}"; + handler.Start(); + await pipe.Writer.WriteAsync(Encoding.UTF8.GetBytes($"Content-Length: {Encoding.UTF8.GetByteCount(content)}\r\n\r\n{content}")); + + await pipe.Writer.CompleteAsync(); + await handler.InputCompleted; + + var action = async () => await pending.Task; + await action.Should().ThrowAsync().Where(x => x.GetType() == exceptionType); + } + + [Fact] + public async Task Should_Process_Mixed_Batch_Items_Independently() + { + var pipe = new Pipe(new PipeOptions()); + var outputHandler = Substitute.For(); + var receiver = new Receiver(); + var responseRouter = new ResponseRouter( + new Lazy(() => outputHandler), + new JsonRpcSerializer(), + new AssemblyScanningHandlerTypeDescriptorProvider(new[] { typeof(AssemblyScanningHandlerTypeDescriptorProvider).Assembly, typeof(InputHandlerTests).Assembly }) + ); + var pending = new TaskCompletionSource(); + responseRouter.Requests.TryAdd("response-id", ("known", pending)).Should().BeTrue(); + + var handlerDescriptor = Substitute.For(); + handlerDescriptor.Method.Returns("known"); + var knownDescriptor = Substitute.For>(); + knownDescriptor.Default.Returns(handlerDescriptor); + var missingDescriptor = Substitute.For>(); + missingDescriptor.Default.Returns((IHandlerDescriptor?)null); + + var requestRouter = Substitute.For>(); + requestRouter.GetDescriptors(Arg.Is(x => x.Method == "known")).Returns(knownDescriptor); + requestRouter.GetDescriptors(Arg.Is(x => x.Method == "missing")).Returns(missingDescriptor); + requestRouter.GetDescriptors(Arg.Is(x => x.Method == "known")).Returns(knownDescriptor); + requestRouter.GetDescriptors(Arg.Is(x => x.Method == "missing")).Returns(missingDescriptor); + + var requestInvoker = Substitute.For(); + requestInvoker + .InvokeRequest(Arg.Any>(), Arg.Any()) + .Returns(x => new RequestInvocationHandle(x.Arg())); + + using var handler = NewHandler( + pipe.Reader, outputHandler, receiver, + requestRouter, + _loggerFactory, responseRouter, + requestInvoker + ); + + const string content = """ + [ + { "jsonrpc": "2.0", "id": "response-id", "result": { "ok": true } }, + { "jsonrpc": "2.0", "id": 1, "method": "known", "params": {} }, + { "jsonrpc": "2.0", "id": 2, "method": "missing", "params": {} }, + { "jsonrpc": "2.0", "method": "missing", "params": {} }, + { "jsonrpc": "2.0", "method": "known", "params": {} } + ] + """; + handler.Start(); + await pipe.Writer.WriteAsync(Encoding.UTF8.GetBytes($"Content-Length: {Encoding.UTF8.GetByteCount(content)}\r\n\r\n{content}")); + + await pipe.Writer.CompleteAsync(); + await handler.InputCompleted; + + pending.Task.IsCompletedSuccessfully.Should().BeTrue(); + requestInvoker.Received(1).InvokeRequest(Arg.Any>(), Arg.Is(x => x.Method == "known")); + requestInvoker.Received(1).InvokeNotification(Arg.Any>(), Arg.Is(x => x.Method == "known")); + outputHandler.Received(1).Send(Arg.Is(x => x.Id!.Equals(2L))); + } + [Fact] public async Task Should_Handle_Header_Terminiator_Being_Incomplete() { diff --git a/test/JsonRpc.Tests/JsonRpc.Tests.csproj b/test/JsonRpc.Tests/JsonRpc.Tests.csproj index 0aab74f9e..4e617e7bf 100644 --- a/test/JsonRpc.Tests/JsonRpc.Tests.csproj +++ b/test/JsonRpc.Tests/JsonRpc.Tests.csproj @@ -1,6 +1,6 @@ - net8.0 + $(TestTargetFrameworks) true AnyCPU diff --git a/test/Lsp.Integration.Tests/Lsp.Integration.Tests.csproj b/test/Lsp.Integration.Tests/Lsp.Integration.Tests.csproj index a1f40f08e..d7a0ed475 100644 --- a/test/Lsp.Integration.Tests/Lsp.Integration.Tests.csproj +++ b/test/Lsp.Integration.Tests/Lsp.Integration.Tests.csproj @@ -1,6 +1,6 @@  - net8.0 + $(TestTargetFrameworks) true AnyCPU diff --git a/test/Lsp.Tests/Capabilities/Client/WorkspaceRefreshCapabilityTests.cs b/test/Lsp.Tests/Capabilities/Client/WorkspaceRefreshCapabilityTests.cs new file mode 100644 index 000000000..3ced6c155 --- /dev/null +++ b/test/Lsp.Tests/Capabilities/Client/WorkspaceRefreshCapabilityTests.cs @@ -0,0 +1,30 @@ +using FluentAssertions; +using Newtonsoft.Json.Linq; +using OmniSharp.Extensions.LanguageServer.Protocol.Client.Capabilities; +using OmniSharp.Extensions.LanguageServer.Protocol.Serialization; +using Xunit; + +namespace Lsp.Tests.Capabilities.Client +{ + public class WorkspaceRefreshCapabilityTests + { + [Fact] + public void Should_Serialize_InlayHint_And_Diagnostic_Workspace_Capabilities_To_Their_Own_Keys() + { + var model = new ClientCapabilities { + Workspace = new WorkspaceClientCapabilities { + InlayHint = new InlayHintWorkspaceClientCapabilities { RefreshSupport = true }, + Diagnostics = new DiagnosticWorkspaceClientCapabilities { RefreshSupport = true } + } + }; + + var result = new LspSerializer(ClientVersion.Lsp3).SerializeObject(model); + + var workspace = JObject.Parse(result)["workspace"]!; + workspace["inlayHint"]!["refreshSupport"]!.Value().Should().BeTrue(); + workspace["diagnostics"]!["refreshSupport"]!.Value().Should().BeTrue(); + workspace["semanticTokens"].Should().BeNull(); + workspace["codeLens"].Should().BeNull(); + } + } +} diff --git a/test/Lsp.Tests/Capabilities/Server/ServerCapabilitiesTests.cs b/test/Lsp.Tests/Capabilities/Server/ServerCapabilitiesTests.cs index fca4c9329..2bc91a456 100644 --- a/test/Lsp.Tests/Capabilities/Server/ServerCapabilitiesTests.cs +++ b/test/Lsp.Tests/Capabilities/Server/ServerCapabilitiesTests.cs @@ -101,5 +101,15 @@ public void Null_Text_Document_Sync(string expected) var deresult = new LspSerializer(ClientVersion.Lsp3).DeserializeObject(expected); deresult.Should().BeEquivalentTo(model, x => x.UsingStructuralRecordEquality()); } + + [Fact] + public void Interface_Exposes_Notebook_Document_Sync() + { + IServerCapabilities capabilities = new ServerCapabilities(); + + capabilities.NotebookDocumentSync = new NotebookDocumentSyncOptions(); + + capabilities.NotebookDocumentSync.Should().NotBeNull(); + } } } diff --git a/test/Lsp.Tests/Lsp.Tests.csproj b/test/Lsp.Tests/Lsp.Tests.csproj index 105dbb540..c1e6116d5 100644 --- a/test/Lsp.Tests/Lsp.Tests.csproj +++ b/test/Lsp.Tests/Lsp.Tests.csproj @@ -1,6 +1,6 @@  - net8.0 + $(TestTargetFrameworks) true AnyCPU diff --git a/test/Lsp.Tests/Models/DiagnosticReportTests.cs b/test/Lsp.Tests/Models/DiagnosticReportTests.cs new file mode 100644 index 000000000..516600b82 --- /dev/null +++ b/test/Lsp.Tests/Models/DiagnosticReportTests.cs @@ -0,0 +1,108 @@ +using System.Collections.Immutable; +using FluentAssertions; +using Newtonsoft.Json.Linq; +using OmniSharp.Extensions.LanguageServer.Protocol; +using OmniSharp.Extensions.LanguageServer.Protocol.Client.Capabilities; +using OmniSharp.Extensions.LanguageServer.Protocol.Models; +using OmniSharp.Extensions.LanguageServer.Protocol.Serialization; +using Xunit; + +namespace Lsp.Tests.Models +{ + public class DiagnosticReportTests + { + private readonly LspSerializer _serializer = new(ClientVersion.Lsp3); + + [Fact] + public void Should_RoundTrip_Document_Diagnostic_Report() + { + DocumentDiagnosticReport model = new FullDocumentDiagnosticReport { + ResultId = "result-1", + Items = new Container( + new Diagnostic { + Message = "message", + Range = new Range(new Position(1, 2), new Position(3, 4)) + } + ) + }; + + var result = _serializer.SerializeObject(model); + + result.Should().Be(@"{""kind"":""full"",""resultId"":""result-1"",""items"":[{""range"":{""start"":{""line"":1,""character"":2},""end"":{""line"":3,""character"":4}},""message"":""message""}]}"); + + var deresult = _serializer.DeserializeObject(result); + deresult.Should().BeOfType(); + var full = (FullDocumentDiagnosticReport)deresult; + full.ResultId.Should().Be("result-1"); + full.Items.Should().ContainSingle().Which.Message.String.Should().Be("message"); + } + + [Fact] + public void Should_RoundTrip_Related_Document_Diagnostic_Report() + { + var relatedUri = DocumentUri.Parse("file:///related.cs"); + RelatedDocumentDiagnosticReport model = new RelatedUnchangedDocumentDiagnosticReport { + ResultId = "result-2", + RelatedDocuments = ImmutableDictionary.Empty.Add( + relatedUri, + new FullDocumentDiagnosticReport { + Items = new Container() + } + ) + }; + + var result = _serializer.SerializeObject(model); + + var json = JObject.Parse(result); + json["kind"]!.Value().Should().Be("unchanged"); + json["resultId"]!.Value().Should().Be("result-2"); + json["relatedDocuments"]![relatedUri.ToString()]!["kind"]!.Value().Should().Be("full"); + + var deresult = _serializer.DeserializeObject(result); + deresult.Should().BeOfType(); + deresult.RelatedDocuments.Should().ContainKey(relatedUri); + deresult.RelatedDocuments![relatedUri].Should().BeOfType(); + } + + [Fact] + public void Should_RoundTrip_Workspace_Diagnostic_Report_Items() + { + WorkspaceDocumentDiagnosticReport model = new WorkspaceFullDocumentDiagnosticReport { + Uri = DocumentUri.Parse("file:///workspace.cs"), + Version = null, + ResultId = "workspace-result", + Items = new Container() + }; + + var result = _serializer.SerializeObject(model); + + result.Should().Be(@"{""kind"":""full"",""uri"":""file:///workspace.cs"",""version"":null,""resultId"":""workspace-result"",""items"":[]}"); + + var deresult = _serializer.DeserializeObject(result); + deresult.Should().BeOfType(); + var full = (WorkspaceFullDocumentDiagnosticReport)deresult; + full.Uri.Should().Be(DocumentUri.Parse("file:///workspace.cs")); + full.Version.Should().BeNull(); + full.ResultId.Should().Be("workspace-result"); + } + + [Fact] + public void Should_Create_Partial_Result_From_Related_Report() + { + var uri = DocumentUri.Parse("file:///related.cs"); + var report = new RelatedFullDocumentDiagnosticReport { + Items = new Container(), + RelatedDocuments = ImmutableDictionary.Empty.Add( + uri, + new UnchangedDocumentDiagnosticReport { ResultId = "same" } + ) + }; + + var partial = DocumentDiagnosticReportPartialResult.From(report); + + partial.Should().NotBeNull(); + partial!.RelatedDocuments.Should().ContainKey(uri); + partial.RelatedDocuments![uri].Should().BeOfType(); + } + } +} diff --git a/test/Lsp.Tests/Models/LSPAnyTests.cs b/test/Lsp.Tests/Models/LSPAnyTests.cs new file mode 100644 index 000000000..78ebb2dc1 --- /dev/null +++ b/test/Lsp.Tests/Models/LSPAnyTests.cs @@ -0,0 +1,56 @@ +using System; +using FluentAssertions; +using Newtonsoft.Json.Linq; +using OmniSharp.Extensions.LanguageServer.Protocol.Client.Capabilities; +using OmniSharp.Extensions.LanguageServer.Protocol.Models; +using OmniSharp.Extensions.LanguageServer.Protocol.Serialization; +using Xunit; + +namespace Lsp.Tests.Models +{ + public class LSPAnyTests + { + [Fact] + public void Deserializes_And_Serializes_Arbitrary_Lsp_Values() + { + const string expected = """ + { + "value": { + "name": "example", + "items": [ + 1, + true, + null + ] + } + } + """; + + var serializer = new LspSerializer(ClientVersion.Lsp3); + var result = serializer.DeserializeObject(expected); + + result.Value.Value.Should().BeOfType(); + Fixture.SerializeObject(result).Should().Be(expected.Replace("\r\n", "\n", StringComparison.Ordinal)); + } + + [Fact] + public void Provides_Lsp_Object_And_Array_Model_Types() + { + var value = LSPAny.From( + new LSPObject + { + ["items"] = new LSPArray(1, "two", false) + } + ); + + value.Value.Should().BeOfType(); + value.Value!["items"].Should().BeOfType(); + value.ToString().Should().Be("""{"items":[1,"two",false]}"""); + } + + private class LSPAnyContainer + { + public LSPAny Value { get; init; } + } + } +} diff --git a/test/Lsp.Tests/Models/Lsp318FeatureTests.cs b/test/Lsp.Tests/Models/Lsp318FeatureTests.cs new file mode 100644 index 000000000..0589980cb --- /dev/null +++ b/test/Lsp.Tests/Models/Lsp318FeatureTests.cs @@ -0,0 +1,197 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using FluentAssertions; +using Newtonsoft.Json.Linq; +using OmniSharp.Extensions.LanguageServer.Protocol; +using OmniSharp.Extensions.LanguageServer.Protocol.Client.Capabilities; +using OmniSharp.Extensions.LanguageServer.Protocol.Models; +using OmniSharp.Extensions.LanguageServer.Protocol.Serialization; +using OmniSharp.Extensions.LanguageServer.Protocol.Server.Capabilities; +using TestingUtils; +using Xunit; + +namespace Lsp.Tests.Models +{ + public class Lsp318FeatureTests + { + [Fact] + public void InlineCompletionList_SupportsItemsAndSnippetInsertText() + { + var model = new InlineCompletionList( + new[] { + new InlineCompletionItem { + InsertText = new StringValue { Value = "Console.WriteLine($1);$0" }, + FilterText = "cw", + Range = new Range(new Position(1, 2), new Position(1, 4)), + Command = new Command { Title = "after", Name = "inline.after", Tooltip = "Run after insertion" } + } + } + ); + + var json = JObject.Parse(Fixture.SerializeObject(model)); + + json["items"]!.Should().HaveCount(1); + json["items"]![0]!["insertText"]!["kind"]!.Value().Should().Be("snippet"); + json["items"]![0]!["insertText"]!["value"]!.Value().Should().Be("Console.WriteLine($1);$0"); + json["items"]![0]!["command"]!["tooltip"]!.Value().Should().Be("Run after insertion"); + + var result = new LspSerializer(ClientVersion.Lsp3).DeserializeObject(json.ToString()); + result.Items.Single().InsertText.StringValue!.Value.Should().Be("Console.WriteLine($1);$0"); + } + + [Fact] + public void InlineCompletionList_DeserializesArrayResultShape() + { + var json = """[{ "insertText": "hello", "filterText": "h" }]"""; + + var result = new LspSerializer(ClientVersion.Lsp3).DeserializeObject(json); + + result.Items.Single().InsertText.String.Should().Be("hello"); + result.Items.Single().FilterText.Should().Be("h"); + } + + [Fact] + public void CompletionList_SerializesApplyKind() + { + var model = new CompletionList(new[] { new CompletionItem { Label = "abc" } }, true) { + ApplyKind = new CompletionItemApplyKinds { + CommitCharacters = ApplyKind.Merge, + Data = ApplyKind.Replace + } + }; + + var json = JObject.Parse(Fixture.SerializeObject(model)); + + json["applyKind"]!["commitCharacters"]!.Value().Should().Be("merge"); + json["applyKind"]!["data"]!.Value().Should().Be("replace"); + + var result = new LspSerializer(ClientVersion.Lsp3).DeserializeObject(json.ToString()); + result.ApplyKind!.CommitCharacters.Should().Be(ApplyKind.Merge); + result.ApplyKind!.Data.Should().Be(ApplyKind.Replace); + } + + [Fact] + public void WorkspaceEdit_SerializesMetadataAndSnippetTextEdit() + { + var model = new WorkspaceEdit { + Metadata = new WorkspaceEditMetadata { IsRefactoring = true }, + DocumentChanges = new Container( + new TextDocumentEdit { + TextDocument = new OptionalVersionedTextDocumentIdentifier { + Uri = new Uri("file:///workspace/test.cs"), + Version = 1 + }, + Edits = new TextEditContainer( + new SnippetTextEdit { + Range = new Range(new Position(0, 0), new Position(0, 0)), + Snippet = new StringValue { Value = "class ${1:Name} {$0}" }, + AnnotationId = "snippet-edit" + } + ) + } + ) + }; + + var json = JObject.Parse(Fixture.SerializeObject(model)); + + json["metadata"]!["isRefactoring"]!.Value().Should().BeTrue(); + json["documentChanges"]![0]!["edits"]![0]!["snippet"]!["kind"]!.Value().Should().Be("snippet"); + json["documentChanges"]![0]!["edits"]![0]!["snippet"]!["value"]!.Value().Should().Be("class ${1:Name} {$0}"); + json["documentChanges"]![0]!["edits"]![0]!["annotationId"]!.Value().Should().Be("snippet-edit"); + + var result = new LspSerializer(ClientVersion.Lsp3).DeserializeObject(json.ToString()); + result.Metadata!.IsRefactoring.Should().BeTrue(); + result.DocumentChanges!.Single().TextDocumentEdit!.Edits.Single().Should().BeOfType(); + } + + [Fact] + public void Capabilities_Serialize318Fields() + { + var capabilities = new ClientCapabilities { + Workspace = new WorkspaceClientCapabilities { + WorkspaceEdit = new WorkspaceEditCapability { + MetadataSupport = true, + SnippetEditSupport = true + }, + FoldingRange = new FoldingRangeWorkspaceClientCapabilities { RefreshSupport = true }, + TextDocumentContent = new TextDocumentContentClientCapabilities { DynamicRegistration = true } + }, + TextDocument = new TextDocumentClientCapabilities { + Filters = new TextDocumentFilterClientCapabilities { RelativePatternSupport = true }, + InlineCompletion = new InlineCompletionClientCapabilities { DynamicRegistration = true }, + RangeFormatting = new DocumentRangeFormattingCapability { RangesSupport = true }, + Completion = new CompletionCapability { + CompletionList = new CompletionListCapabilityOptions { ApplyKindSupport = true } + }, + CodeAction = new CodeActionCapability { DocumentationSupport = true }, + CodeLens = new CodeLensCapability { + ResolveSupport = new CodeLensCapabilityResolveSupport { Properties = new Container("command") } + }, + SignatureHelp = new SignatureHelpCapability { + SignatureInformation = new SignatureInformationCapabilityOptions { NoActiveParameterSupport = true } + }, + Diagnostic = new DiagnosticClientCapabilities { MarkupMessageSupport = true } + } + }; + + var json = JObject.Parse(Fixture.SerializeObject(capabilities)); + + json["workspace"]!["workspaceEdit"]!["metadataSupport"]!.Value().Should().BeTrue(); + json["workspace"]!["workspaceEdit"]!["snippetEditSupport"]!.Value().Should().BeTrue(); + json["workspace"]!["foldingRange"]!["refreshSupport"]!.Value().Should().BeTrue(); + json["workspace"]!["textDocumentContent"]!["dynamicRegistration"]!.Value().Should().BeTrue(); + json["textDocument"]!["filters"]!["relativePatternSupport"]!.Value().Should().BeTrue(); + json["textDocument"]!["inlineCompletion"]!["dynamicRegistration"]!.Value().Should().BeTrue(); + json["textDocument"]!["rangeFormatting"]!["rangesSupport"]!.Value().Should().BeTrue(); + json["textDocument"]!["completion"]!["completionList"]!["applyKindSupport"]!.Value().Should().BeTrue(); + json["textDocument"]!["codeAction"]!["documentationSupport"]!.Value().Should().BeTrue(); + json["textDocument"]!["codeLens"]!["resolveSupport"]!["properties"]!.Values().Should().Contain("command"); + json["textDocument"]!["signatureHelp"]!["signatureInformation"]!["noActiveParameterSupport"]!.Value().Should().BeTrue(); + json["textDocument"]!["diagnostic"]!["markupMessageSupport"]!.Value().Should().BeTrue(); + } + + [Fact] + public void ServerCapabilities_Serialize318Providers() + { + var capabilities = new ServerCapabilities { + InlineCompletionProvider = new InlineCompletionRegistrationOptions.StaticOptions { + WorkDoneProgress = true + }, + DocumentRangeFormattingProvider = new DocumentRangeFormattingRegistrationOptions.StaticOptions { + RangesSupport = true + }, + Workspace = new WorkspaceServerCapabilities { + TextDocumentContent = new TextDocumentContentRegistrationOptions.StaticOptions { + Schemes = new Container("git", "vscode-notebook-cell") + } + }, + CodeActionProvider = new CodeActionRegistrationOptions.StaticOptions { + Documentation = new Container( + new CodeActionKindDocumentation { + Kind = CodeActionKind.RefactorMove, + Command = new Command { Title = "Move help", Name = "help.move", Tooltip = "Learn about move refactorings" } + } + ) + } + }; + + var json = JObject.Parse(Fixture.SerializeObject(capabilities)); + + json["inlineCompletionProvider"]!["workDoneProgress"]!.Value().Should().BeTrue(); + json["documentRangeFormattingProvider"]!["rangesSupport"]!.Value().Should().BeTrue(); + json["workspace"]!["textDocumentContent"]!["schemes"]!.Values().Should().Contain(new[] { "git", "vscode-notebook-cell" }); + json["codeActionProvider"]!["documentation"]![0]!["kind"]!.Value().Should().Be("refactor.move"); + json["codeActionProvider"]!["documentation"]![0]!["command"]!["tooltip"]!.Value().Should().Be("Learn about move refactorings"); + } + + [Fact] + public void Enumerations_Expose318Values() + { + ((int)MessageType.Debug).Should().Be(5); + CodeActionKind.RefactorMove.ToString().Should().Be("refactor.move"); + CodeActionKind.Notebook.ToString().Should().Be("notebook"); + SemanticTokenType.Label.ToString().Should().Be("label"); + } + } +} diff --git a/test/Lsp.Tests/Models/NotebookDocumentChangeEventTests.cs b/test/Lsp.Tests/Models/NotebookDocumentChangeEventTests.cs new file mode 100644 index 000000000..d9b7753da --- /dev/null +++ b/test/Lsp.Tests/Models/NotebookDocumentChangeEventTests.cs @@ -0,0 +1,116 @@ +using System; +using System.Linq; +using FluentAssertions; +using Newtonsoft.Json.Linq; +using OmniSharp.Extensions.LanguageServer.Protocol.Client.Capabilities; +using OmniSharp.Extensions.LanguageServer.Protocol.Models; +using OmniSharp.Extensions.LanguageServer.Protocol.Serialization; +using Xunit; + +namespace Lsp.Tests.Models +{ + public class NotebookDocumentChangeEventTests + { + [Fact] + public void Deserializes_Text_Content_As_Array() + { + const string expected = """ + { + "cells": { + "textContent": [ + { + "document": { + "uri": "file:///notebook/cell1.cs", + "version": 2 + }, + "changes": [ + { + "text": "one" + } + ] + }, + { + "document": { + "uri": "file:///notebook/cell2.cs", + "version": 3 + }, + "changes": [ + { + "text": "two" + } + ] + } + ] + } + } + """; + + var result = new LspSerializer(ClientVersion.Lsp3).DeserializeObject(expected); + + result.Cells.Should().NotBeNull(); + result.Cells!.TextContent.Should().NotBeNull(); + result.Cells.TextContent!.ToArray().Should().HaveCount(2); + JToken.DeepEquals(JObject.Parse(Fixture.SerializeObject(result)), JObject.Parse(expected)).Should().BeTrue(); + } + + [Fact] + public void Serializes_Text_Content_As_Array() + { + var model = new NotebookDocumentChangeEvent + { + Cells = new NotebookDocumentChangeEventCells + { + TextContent = new Container( + new NotebookDocumentChangeEventCellsTextContent + { + Document = new VersionedTextDocumentIdentifier + { + Uri = new Uri("file:///notebook/cell1.cs"), + Version = 2 + }, + Changes = new Container( + new TextDocumentContentChangeEvent + { + Text = "one" + } + ) + }, + new NotebookDocumentChangeEventCellsTextContent + { + Document = new VersionedTextDocumentIdentifier + { + Uri = new Uri("file:///notebook/cell2.cs"), + Version = 3 + }, + Changes = new Container( + new TextDocumentContentChangeEvent + { + Text = "two" + } + ) + } + ) + } + }; + + var result = JObject.Parse(Fixture.SerializeObject(model)); + + result["cells"]!["textContent"].Should().BeOfType(); + result["cells"]!["textContent"]!.Should().HaveCount(2); + } + + [Fact] + public void Omits_Cells_When_Only_Metadata_Changes() + { + var model = new NotebookDocumentChangeEvent + { + Metadata = JObject.Parse("""{ "custom": true }""") + }; + + var result = JObject.Parse(Fixture.SerializeObject(model)); + + result["metadata"]!["custom"]!.Value().Should().BeTrue(); + result.ContainsKey("cells").Should().BeFalse(); + } + } +} diff --git a/test/Lsp.Tests/Models/TypeHierarchyTests.cs b/test/Lsp.Tests/Models/TypeHierarchyTests.cs new file mode 100644 index 000000000..796c286bf --- /dev/null +++ b/test/Lsp.Tests/Models/TypeHierarchyTests.cs @@ -0,0 +1,40 @@ +using FluentAssertions; +using OmniSharp.Extensions.LanguageServer.Protocol.Models; +using Xunit; + +namespace Lsp.Tests.Models +{ + public class TypeHierarchyTests + { + [Fact] + public void Subtypes_Typed_Conversions_Preserve_Distinct_Progress_Tokens() + { + var parameters = new TypeHierarchySubtypesParams + { + Item = new TypeHierarchyItem + { + Name = "C", + Kind = SymbolKind.Class, + Uri = "file:///workspace/c.cs", + Range = new Range(new Position(1, 2), new Position(3, 4)), + SelectionRange = new Range(new Position(1, 2), new Position(1, 3)) + }, + PartialResultToken = "partial", + WorkDoneToken = "work" + }; + + var typed = TypeHierarchySubtypesParams.Create(parameters); + var untyped = TypeHierarchySubtypesParams.Create(typed); + + typed.PartialResultToken.Should().Be(parameters.PartialResultToken); + typed.WorkDoneToken.Should().Be(parameters.WorkDoneToken); + untyped.PartialResultToken.Should().Be(parameters.PartialResultToken); + untyped.WorkDoneToken.Should().Be(parameters.WorkDoneToken); + } + + private class TestHandlerIdentity : IHandlerIdentity + { + public string __identity { get; init; } = string.Empty; + } + } +} diff --git a/test/TestingUtils/TestingUtils.csproj b/test/TestingUtils/TestingUtils.csproj index e8296c701..ae4584dd7 100644 --- a/test/TestingUtils/TestingUtils.csproj +++ b/test/TestingUtils/TestingUtils.csproj @@ -1,8 +1,8 @@  - net8.0 - false + $(TestTargetFrameworks) + false diff --git a/test/coverage/Client-Tests/coverage.net10.0.info b/test/coverage/Client-Tests/coverage.net10.0.info new file mode 100644 index 000000000..e69de29bb diff --git a/test/coverage/Client-Tests/coverage.net10.0.json b/test/coverage/Client-Tests/coverage.net10.0.json new file mode 100644 index 000000000..9789d2ac6 --- /dev/null +++ b/test/coverage/Client-Tests/coverage.net10.0.json @@ -0,0 +1,9 @@ +{ + "OmniSharp.Extensions.JsonRpc.dll": {}, + "OmniSharp.Extensions.JsonRpc.Testing.dll": {}, + "OmniSharp.Extensions.LanguageClient.dll": {}, + "OmniSharp.Extensions.LanguageProtocol.dll": {}, + "OmniSharp.Extensions.LanguageProtocol.Testing.dll": {}, + "OmniSharp.Extensions.LanguageServer.dll": {}, + "OmniSharp.Extensions.LanguageServer.Shared.dll": {} +} \ No newline at end of file diff --git a/test/coverage/Dap-Tests/coverage.net10.0.info b/test/coverage/Dap-Tests/coverage.net10.0.info new file mode 100644 index 000000000..e69de29bb diff --git a/test/coverage/Dap-Tests/coverage.net10.0.json b/test/coverage/Dap-Tests/coverage.net10.0.json new file mode 100644 index 000000000..fc235a3be --- /dev/null +++ b/test/coverage/Dap-Tests/coverage.net10.0.json @@ -0,0 +1,11 @@ +{ + "OmniSharp.Extensions.DebugAdapter.Client.dll": {}, + "OmniSharp.Extensions.DebugAdapter.dll": {}, + "OmniSharp.Extensions.DebugAdapter.Proposals.dll": {}, + "OmniSharp.Extensions.DebugAdapter.Server.dll": {}, + "OmniSharp.Extensions.DebugAdapter.Shared.dll": {}, + "OmniSharp.Extensions.DebugAdapter.Testing.dll": {}, + "OmniSharp.Extensions.JsonRpc.dll": {}, + "OmniSharp.Extensions.JsonRpc.Generators.dll": {}, + "OmniSharp.Extensions.JsonRpc.Testing.dll": {} +} \ No newline at end of file diff --git a/test/coverage/Generation-Tests/coverage.net10.0.info b/test/coverage/Generation-Tests/coverage.net10.0.info new file mode 100644 index 000000000..e69de29bb diff --git a/test/coverage/Generation-Tests/coverage.net10.0.json b/test/coverage/Generation-Tests/coverage.net10.0.json new file mode 100644 index 000000000..9182989ca --- /dev/null +++ b/test/coverage/Generation-Tests/coverage.net10.0.json @@ -0,0 +1,7 @@ +{ + "OmniSharp.Extensions.DebugAdapter.dll": {}, + "OmniSharp.Extensions.JsonRpc.dll": {}, + "OmniSharp.Extensions.JsonRpc.Generators.dll": {}, + "OmniSharp.Extensions.JsonRpc.Testing.dll": {}, + "OmniSharp.Extensions.LanguageProtocol.dll": {} +} \ No newline at end of file diff --git a/test/coverage/JsonRpc-Tests/coverage.net10.0.info b/test/coverage/JsonRpc-Tests/coverage.net10.0.info new file mode 100644 index 000000000..e69de29bb diff --git a/test/coverage/JsonRpc-Tests/coverage.net10.0.json b/test/coverage/JsonRpc-Tests/coverage.net10.0.json new file mode 100644 index 000000000..bd2a5f25e --- /dev/null +++ b/test/coverage/JsonRpc-Tests/coverage.net10.0.json @@ -0,0 +1,4 @@ +{ + "OmniSharp.Extensions.JsonRpc.dll": {}, + "OmniSharp.Extensions.JsonRpc.Testing.dll": {} +} \ No newline at end of file diff --git a/test/coverage/Lsp-Integration-Tests/coverage.net10.0.info b/test/coverage/Lsp-Integration-Tests/coverage.net10.0.info new file mode 100644 index 000000000..e69de29bb diff --git a/test/coverage/Lsp-Integration-Tests/coverage.net10.0.json b/test/coverage/Lsp-Integration-Tests/coverage.net10.0.json new file mode 100644 index 000000000..4a0dd0973 --- /dev/null +++ b/test/coverage/Lsp-Integration-Tests/coverage.net10.0.json @@ -0,0 +1,11 @@ +{ + "OmniSharp.Extensions.JsonRpc.dll": {}, + "OmniSharp.Extensions.JsonRpc.Generators.dll": {}, + "OmniSharp.Extensions.JsonRpc.Testing.dll": {}, + "OmniSharp.Extensions.LanguageClient.dll": {}, + "OmniSharp.Extensions.LanguageProtocol.dll": {}, + "OmniSharp.Extensions.LanguageProtocol.Proposals.dll": {}, + "OmniSharp.Extensions.LanguageProtocol.Testing.dll": {}, + "OmniSharp.Extensions.LanguageServer.dll": {}, + "OmniSharp.Extensions.LanguageServer.Shared.dll": {} +} \ No newline at end of file diff --git a/test/coverage/Lsp-Tests/coverage.net10.0.info b/test/coverage/Lsp-Tests/coverage.net10.0.info new file mode 100644 index 000000000..e69de29bb diff --git a/test/coverage/Lsp-Tests/coverage.net10.0.json b/test/coverage/Lsp-Tests/coverage.net10.0.json new file mode 100644 index 000000000..4a0dd0973 --- /dev/null +++ b/test/coverage/Lsp-Tests/coverage.net10.0.json @@ -0,0 +1,11 @@ +{ + "OmniSharp.Extensions.JsonRpc.dll": {}, + "OmniSharp.Extensions.JsonRpc.Generators.dll": {}, + "OmniSharp.Extensions.JsonRpc.Testing.dll": {}, + "OmniSharp.Extensions.LanguageClient.dll": {}, + "OmniSharp.Extensions.LanguageProtocol.dll": {}, + "OmniSharp.Extensions.LanguageProtocol.Proposals.dll": {}, + "OmniSharp.Extensions.LanguageProtocol.Testing.dll": {}, + "OmniSharp.Extensions.LanguageServer.dll": {}, + "OmniSharp.Extensions.LanguageServer.Shared.dll": {} +} \ No newline at end of file diff --git a/test/coverage/TestingUtils/coverage.net10.0.info b/test/coverage/TestingUtils/coverage.net10.0.info new file mode 100644 index 000000000..e69de29bb diff --git a/test/coverage/TestingUtils/coverage.net10.0.json b/test/coverage/TestingUtils/coverage.net10.0.json new file mode 100644 index 000000000..bd2a5f25e --- /dev/null +++ b/test/coverage/TestingUtils/coverage.net10.0.json @@ -0,0 +1,4 @@ +{ + "OmniSharp.Extensions.JsonRpc.dll": {}, + "OmniSharp.Extensions.JsonRpc.Testing.dll": {} +} \ No newline at end of file