(* The Sol language server: JSON-RPC over stdio, hand rolled on
yojson. One capability, the load-bearing one: diagnostics. Every
edit runs the same admission the runtime runs, so a rule with no
decrease witness or a fact violating its schema is an error in
the editor, at typing time, with the same message the loader
gives. Parse errors carry positions; admission errors anchor at
the top of the file until the loader learns locations. *)
let log_json json = Yojson.Safe.to_string json
let send json =
let body = log_json json in
Printf.printf "Content-Length: %d\r\n\r\n%s" (String.length body) body;
flush stdout
let read_message () =
let rec headers length =
match In_channel.input_line stdin with
| None -> None
| Some line -> (
let line = String.trim line in
if line = "" then length
else
match String.index_opt line ':' with
| Some i
when String.lowercase_ascii (String.sub line 0 i) = "content-length"
->
headers
(int_of_string_opt
(String.trim
(String.sub line (i + 1) (String.length line - i - 1))))
| _ -> headers length)
in
match headers None with
| None -> None
| Some length -> (
match really_input_string stdin length with
| body -> ( try Some (Yojson.Safe.from_string body) with _ -> None)
| exception End_of_file -> None)
let member name json =
match json with
| `Assoc fields -> Option.value ~default:`Null (List.assoc_opt name fields)
| _ -> `Null
let to_string_opt = function `String s -> Some s | _ -> None
(* Everything the checker can say about a document, as LSP
diagnostics: (line, column, message), zero-based lines. *)
let diagnostics text =
match Sol.Front.parse text with
| Error { Sol.Front.line; column; message } ->
[ (max 0 (line - 1), max 0 column, message) ]
| Ok statements -> (
match Sol.Program.load statements with
| Error error -> [ (0, 0, Format.asprintf "%a" Sol.Program.pp_error error) ]
| Ok _ -> [])
let publish uri text =
let items =
List.map
(fun (line, column, message) ->
`Assoc
[
( "range",
`Assoc
[
( "start",
`Assoc [ ("line", `Int line); ("character", `Int column) ]
);
( "end",
`Assoc
[ ("line", `Int line); ("character", `Int (column + 1)) ]
);
] );
("severity", `Int 1);
("source", `String "sol");
("message", `String message);
])
(diagnostics text)
in
send
(`Assoc
[
("jsonrpc", `String "2.0");
("method", `String "textDocument/publishDiagnostics");
("params", `Assoc [ ("uri", `String uri); ("diagnostics", `List items) ]);
])
let respond id result =
send (`Assoc [ ("jsonrpc", `String "2.0"); ("id", id); ("result", result) ])
let check_notification params =
let uri =
to_string_opt (member "uri" (member "textDocument" params))
in
let text =
match to_string_opt (member "text" (member "textDocument" params)) with
| Some text -> Some text
| None -> (
(* didChange, full sync: the last full content change. *)
match member "contentChanges" params with
| `List changes -> (
match List.rev changes with
| last :: _ -> to_string_opt (member "text" last)
| [] -> None)
| _ -> None)
in
match (uri, text) with Some uri, Some text -> publish uri text | _ -> ()
let run () =
set_binary_mode_in stdin true;
set_binary_mode_out stdout true;
let running = ref true in
while !running do
match read_message () with
| None -> running := false
| Some message -> (
let id = member "id" message in
let params = member "params" message in
match to_string_opt (member "method" message) with
| Some "initialize" ->
respond id
(`Assoc
[
( "capabilities",
`Assoc [ ("textDocumentSync", `Int 1) ] );
( "serverInfo",
`Assoc
[
("name", `String "sol-lsp"); ("version", `String "0.1.0");
] );
])
| Some "shutdown" -> respond id `Null
| Some "exit" -> running := false
| Some ("textDocument/didOpen" | "textDocument/didChange") ->
check_notification params
| Some "textDocument/didClose" -> (
match to_string_opt (member "uri" (member "textDocument" params)) with
| Some uri ->
send
(`Assoc
[
("jsonrpc", `String "2.0");
("method", `String "textDocument/publishDiagnostics");
( "params",
`Assoc
[ ("uri", `String uri); ("diagnostics", `List []) ] );
])
| None -> ())
| Some _ when id <> `Null ->
(* An unsupported request deserves an answer, not silence. *)
send
(`Assoc
[
("jsonrpc", `String "2.0");
("id", id);
( "error",
`Assoc
[
("code", `Int (-32601));
("message", `String "method not found");
] );
])
| _ -> ())
done