90 lines
3.3 KiB
Forth
90 lines
3.3 KiB
Forth
|
|
/// 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()
|