Runs the compiler in-process against the Bjolang checkout beside it, and answers diagnostics, an outline, go to definition, hover and completion, plus a request of its own for macro expansion. Diagnostics come from the compiler's own check of the editor's text, saved or not: on open, on save, and 400 ms after typing pauses. The outline, definitions, hovers and completions read only as far as the reader, with no type checking, so they answer on every request: the top-level names of the file, its imports and the prelude, local names from the binding forms the parser knows, the declarations and docs as written, and the builtins with their types. emacs/bjolang-lsp.el shows a macro call's expansion, one step or all of it, in a buffer. test/smoke.py drives the server as an editor would. 🤖 Generated with [ECA](https://eca.dev) Co-Authored-By: eca-agent <git@eca.dev>
89 lines
3.3 KiB
FSharp
89 lines
3.3 KiB
FSharp
/// 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()
|