lsp-bjolang/Rpc.fs

90 lines
3.3 KiB
Forth
Raw Normal View History

/// 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<byte> 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()