Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions src/Client.fs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ type LspClient() =

/// The telemetry notification is sent from the server to the client to ask the client to log
/// a telemetry event.
abstract member TelemetryEvent: Newtonsoft.Json.Linq.JToken -> Async<unit>
abstract member TelemetryEvent: LSPAny -> Async<unit>

default __.TelemetryEvent(_) = ignoreNotification

Expand Down Expand Up @@ -81,7 +81,7 @@ type LspClient() =
/// The request can fetch n configuration settings in one roundtrip. The order of the returned configuration
/// settings correspond to the order of the passed ConfigurationItems (e.g. the first item in the response
/// is the result for the first configuration item in the params).
abstract member WorkspaceConfiguration: ConfigurationParams -> AsyncLspResult<Newtonsoft.Json.Linq.JToken[]>
abstract member WorkspaceConfiguration: ConfigurationParams -> AsyncLspResult<LSPAny[]>

default __.WorkspaceConfiguration(_) = notImplemented

Expand Down Expand Up @@ -172,7 +172,7 @@ type LspClient() =
member this.WindowShowMessageRequest(p: ShowMessageRequestParams) = this.WindowShowMessageRequest(p)
member this.WindowLogMessage(p: LogMessageParams) = this.WindowLogMessage(p)
member this.WindowShowDocument(p: ShowDocumentParams) = this.WindowShowDocument(p)
member this.TelemetryEvent(p: Newtonsoft.Json.Linq.JToken) = this.TelemetryEvent(p)
member this.TelemetryEvent(p: LSPAny) = this.TelemetryEvent(p)
member this.ClientRegisterCapability(p: RegistrationParams) = this.ClientRegisterCapability(p)
member this.ClientUnregisterCapability(p: UnregistrationParams) = this.ClientUnregisterCapability(p)
member this.WorkspaceWorkspaceFolders() = this.WorkspaceWorkspaceFolders()
Expand Down
1 change: 1 addition & 0 deletions src/Ionide.LanguageServerProtocol.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
<PackageReference Include="DotNet.ReproducibleBuilds" Version="1.1.1" PrivateAssets="All" />
<PackageReference Include="Ionide.KeepAChangelog.Tasks" Version="0.1.8" PrivateAssets="All" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="System.Text.Json" Version="10.0.8" />
<!-- Explicitly pinning our FSharp.Core to 6.0.0 so that consumers can use _any_ 6.x version. -->
<PackageReference Update="FSharp.Core" Version="6.0.0" />
<PackageReference Include="StreamJsonRpc" Version="2.16.36" />
Expand Down
7 changes: 5 additions & 2 deletions src/LanguageServerProtocol.fs
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,11 @@ module Server =

let jsonRpcFormatter = defaultJsonRpcFormatter ()

let deserialize<'t> (token: JToken) = token.ToObject<'t>(jsonRpcFormatter.JsonSerializer)
let serialize<'t> (o: 't) = JToken.FromObject(o, jsonRpcFormatter.JsonSerializer)
let deserialize<'t> (value: LSPAny) = value.JToken.ToObject<'t>(jsonRpcFormatter.JsonSerializer)

let serialize<'t> (o: 't) =
JToken.FromObject(o, jsonRpcFormatter.JsonSerializer)
|> LSPAny

let requestHandling<'param, 'result> (run: 'param -> AsyncLspResult<'result>) : Delegate =
let runAsTask param ct =
Expand Down
7 changes: 0 additions & 7 deletions src/Types.cg.fs
Original file line number Diff line number Diff line change
Expand Up @@ -6438,13 +6438,6 @@ type DefinitionLink = LocationLink
/// LSP arrays.
/// @since 3.17.0
type LSPArray = LSPAny[]
/// The LSP any type.
/// Please note that strictly speaking a property with the value `undefined`
/// can't be converted into JSON preserving the property name. However for
/// convenience it is allowed and assumed that all these properties are
/// optional as well.
/// @since 3.17.0
type LSPAny = JToken
/// The declaration of a symbol representation as one or many {@link Location locations}.
type Declaration = U2<Location, Location[]>
/// Information about where a symbol is declared.
Expand Down
67 changes: 65 additions & 2 deletions src/Types.fs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
namespace Ionide.LanguageServerProtocol.Types

open Ionide.LanguageServerProtocol

open Newtonsoft.Json
open Newtonsoft.Json.Linq

/// Types in typescript can have hardcoded values for their fields, this attribute is used to mark
/// the default value for a field in a type and is used when deserializing the type to json
Expand Down Expand Up @@ -89,4 +90,66 @@ type U4<'T1, 'T2, 'T3, 'T4> =
| C1 c -> string c
| C2 c -> string c
| C3 c -> string 3
| C4 c -> string 3
| C4 c -> string 3

/// The LSP any type.
///
/// Wraps a <see cref="JToken"/> and provides structural equality and hashing so that values
/// can safely be used in sets, maps, and comparisons.
///
/// Note: structural equality and hashing are implemented here explicitly because while
/// <see cref="Newtonsoft.Json.Linq.JValue"/> does provide structural equality for primitive values,
/// <see cref="Newtonsoft.Json.Linq.JObject"/> and <see cref="Newtonsoft.Json.Linq.JArray"/>
/// have a known-broken <c>GetHashCode</c> that can return different values for structurally equal
/// instances. <c>System.Text.Json.JsonElement</c> — the intended future backing type — provides
/// neither structural equality nor hashing at all. This wrapper is therefore necessary regardless
/// of which JSON library is used underneath.
///
/// The internal representation is intentionally kept behind a single <c>JToken</c> property so
/// that the backing type can be swapped to <c>System.Text.Json.JsonElement</c> in the future
/// with minimal impact on call sites. Prefer the <c>fromJToken</c> / <c>fromJsonElement</c>
/// factories over the constructor directly.
[<JsonConverter(typeof<LSPAnyConverter>)>]
type LSPAny(token: JToken) =

/// The underlying JSON token.
member _.JToken: JToken = token

/// The value as a <see cref="System.Text.Json.JsonElement"/>, bridged via raw JSON text.
/// Once the backing type is migrated to <see cref="System.Text.Json.JsonElement"/> this will be a direct accessor.
member _.JsonElement: System.Text.Json.JsonElement =
System.Text.Json.JsonSerializer.Deserialize<System.Text.Json.JsonElement>(token.ToString(Formatting.None))

override _.ToString() = token.ToString(Formatting.None)

override _.GetHashCode() =
// JToken does not override GetHashCode, so we compute one from the raw JSON text.
// This is consistent with the Equals implementation below (same raw text ↔ same hash).
token.ToString(Formatting.None).GetHashCode()

override x.Equals(obj) =
match obj with
| :? LSPAny as other -> JToken.DeepEquals(token, other.JToken)
| _ -> false

interface System.IEquatable<LSPAny> with
member x.Equals(other) = JToken.DeepEquals(token, other.JToken)

/// Wraps a <see cref="JToken"/> in an <see cref="LSPAny"/>.
static member inline fromJToken(token: JToken) = LSPAny(token)

/// Wraps a <see cref="System.Text.Json.JsonElement"/> in an <see cref="LSPAny"/>, bridged via raw JSON text.
/// Once the backing type is migrated to <see cref="System.Text.Json.JsonElement"/> this will be a direct wrap.
static member inline fromJsonElement(element: System.Text.Json.JsonElement) =
LSPAny(JToken.Parse(element.GetRawText()))

/// Newtonsoft.Json converter for <see cref="LSPAny"/>.
/// Reads any JSON value into a <see cref="JToken"/> and wraps it; writes by delegating to the token.
and LSPAnyConverter() =
inherit JsonConverter()

override _.CanConvert(t) = t = typeof<LSPAny>

override _.ReadJson(reader, _t, _existing, _serializer) = LSPAny(JToken.ReadFrom(reader)) :> obj

override _.WriteJson(writer, value, _serializer) = (value :?> LSPAny).JToken.WriteTo(writer)
16 changes: 10 additions & 6 deletions tests/Benchmarks.fs
Original file line number Diff line number Diff line change
Expand Up @@ -679,7 +679,11 @@ type MultipleTypesBenchmarks() =
Tooltip = Some(U2.C2 { Kind = MarkupKind.PlainText; Value = "some tooltip" })
PaddingLeft = Some true
PaddingRight = Some false
Data = Some(JToken.FromObject "some data")
Data =
Some(
JToken.FromObject "some data"
|> LSPAny.fromJToken
)
}

let allLsp: obj[] = [|
Expand Down Expand Up @@ -707,7 +711,7 @@ type MultipleTypesBenchmarks() =
inlayHint
|> serialize

let res = json.ToObject(o.GetType(), jsonRpcFormatter.JsonSerializer)
let res = json.JToken.ToObject(o.GetType(), jsonRpcFormatter.JsonSerializer)
()

[<BenchmarkCategory("LSP"); Benchmark>]
Expand All @@ -722,7 +726,7 @@ type MultipleTypesBenchmarks() =
example
|> serialize

let res = json.ToObject(example.GetType(), jsonRpcFormatter.JsonSerializer)
let res = json.JToken.ToObject(example.GetType(), jsonRpcFormatter.JsonSerializer)
()

[<BenchmarkCategory("Example"); Benchmark>]
Expand All @@ -741,7 +745,7 @@ type MultipleTypesBenchmarks() =
option
|> serialize

let _ = json.ToObject(option.GetType(), jsonRpcFormatter.JsonSerializer)
let _ = json.JToken.ToObject(option.GetType(), jsonRpcFormatter.JsonSerializer)
()

member _.SingleCaseUnion_ArgumentsSource() =
Expand All @@ -762,7 +766,7 @@ type MultipleTypesBenchmarks() =
data
|> serialize

let _ = json.ToObject(typeof<Example.SingleCaseUnion>, jsonRpcFormatter.JsonSerializer)
let _ = json.JToken.ToObject(typeof<Example.SingleCaseUnion>, jsonRpcFormatter.JsonSerializer)
()

member _.ErasedUnion_ArgumentsSource() =
Expand Down Expand Up @@ -791,7 +795,7 @@ type MultipleTypesBenchmarks() =
data
|> serialize

let _ = json.ToObject(typeof<Example.ErasedUnionData>, jsonRpcFormatter.JsonSerializer)
let _ = json.JToken.ToObject(typeof<Example.ErasedUnionData>, jsonRpcFormatter.JsonSerializer)
()


Expand Down
Loading
Loading