/// JSON-RPC over stdin and stdout, framed as the Language Server Protocol /// frames it: a `Content-Length` header, a blank line, and that many bytes of /// UTF-8 JSON. /// /// The streams are the process's raw ones, opened once. The compiler redirects /// `Console.Out` while it checks, and a message written through `Console` /// then would land in its capture buffer instead of with the client. module BjolangLsp.Rpc open System open System.IO open System.Text open System.Text.Json.Nodes let private input = Console.OpenStandardInput() let private output = Console.OpenStandardOutput() let private writeLock = obj () /// One header line, without its line ending. `None` at end of input. let private readHeaderLine () : string option = let line = StringBuilder() let rec go () = match input.ReadByte() with | -1 -> if line.Length = 0 then None else Some(line.ToString()) | 10 -> Some(line.ToString().TrimEnd '\r') | b -> line.Append(char b) |> ignore go () go () /// The next message. `None` when the client has closed stdin. let read () : JsonNode option = let rec headers (length: int option) = match readHeaderLine () with | None -> None | Some "" -> // A blank line without a length is a malformed frame. There is no // way to find the next one, so it ends the session like end of input. length | Some line -> match line.Split(':', 2) with | [| name; value |] when name.Trim().Equals("Content-Length", StringComparison.OrdinalIgnoreCase) -> headers (Some(int (value.Trim()))) | _ -> headers length headers None |> Option.bind (fun length -> let body = Array.zeroCreate length let mutable got = 0 while got < length do match input.Read(body, got, length - got) with | 0 -> raise (EndOfStreamException "stdin closed inside a message") | n -> got <- got + n Some(JsonNode.Parse(Encoding.UTF8.GetString body))) let private write (message: JsonObject) = message["jsonrpc"] <- "2.0" let body = Encoding.UTF8.GetBytes(message.ToJsonString()) let header = Encoding.ASCII.GetBytes($"Content-Length: %d{body.Length}\r\n\r\n") lock writeLock (fun () -> output.Write(header, 0, header.Length) output.Write(body, 0, body.Length) output.Flush()) /// The answer to request `id`. let respond (id: JsonNode) (result: JsonNode) = write (JsonObject(dict [ "id", id.DeepClone(); "result", result ])) /// An error answer to request `id`. let respondError (id: JsonNode) (code: int) (message: string) = let error = JsonObject(dict [ "code", JsonValue.Create code :> JsonNode; "message", JsonValue.Create message ]) write (JsonObject(dict [ "id", id.DeepClone(); "error", error :> JsonNode ])) /// A notification to the client. let notify (methodName: string) (parameters: JsonNode) = write (JsonObject(dict [ "method", JsonValue.Create methodName :> JsonNode; "params", parameters ])) /// A line for the server's log, which is stderr: the client shows it, and it /// never mixes with the protocol on stdout. let log (message: string) = let err = Console.OpenStandardError() let bytes = Encoding.UTF8.GetBytes(message + "\n") err.Write(bytes, 0, bytes.Length) err.Flush()