-
Notifications
You must be signed in to change notification settings - Fork 17
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Basic language server implementation #121
base: master
Are you sure you want to change the base?
Changes from 15 commits
c57d579
99a00f1
8b46f4e
62e8077
d4a8edb
22d80fb
43f6c0d
f9cdff8
63563c9
3c3d1ab
01f2147
2b22275
7486c8e
36d7767
f6841d0
a884b5b
f68b29c
40d8d0e
952bdff
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||
---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,69 @@ | ||||||||
(* This file is part of DBL, released under MIT license. | ||||||||
* See LICENSE for details. | ||||||||
*) | ||||||||
|
||||||||
(** Base HTTP-like protocol *) | ||||||||
|
||||||||
(** The JSON-RPC messages are sent over this simple protocol. | ||||||||
It consists of a header and a content part, separated by "\r\n": | ||||||||
header1: value1\r\n | ||||||||
header2: value2\r\n | ||||||||
\r\n | ||||||||
[JSON-RPC message] | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (suggestion might be badly formatted)
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's an implementation detail (not stated in the specification) and the top comment wasn't meant for that. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fair enough. |
||||||||
The only recognized headers are Content-Length and Content-Type | ||||||||
and there is really one possible value for Content-Type, | ||||||||
so we don't care about that. *) | ||||||||
|
||||||||
open In_channel | ||||||||
open Out_channel | ||||||||
|
||||||||
exception Connection_error of string | ||||||||
|
||||||||
type headers = | ||||||||
{ content_length: int option | ||||||||
; content_type: string option | ||||||||
} | ||||||||
|
||||||||
let parse_header headers line = | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This sounds like something we could use a third party library for unless it's pretty custom. (could refactor later) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not a fan of dependencies and the whole file is like 60 lines (and does exactly what's needed), so I'm not sure how much a library would help. |
||||||||
match String.split_on_char ':' line |> List.map String.trim with | ||||||||
| ["Content-Length"; value] -> begin | ||||||||
match int_of_string_opt value with | ||||||||
| None -> raise (Connection_error ("Invalid Content-Length value: " ^ line)) | ||||||||
| Some len -> { headers with content_length = Some len } | ||||||||
end | ||||||||
| ["Content-Type"; value] -> { headers with content_type = Some value } | ||||||||
| _ -> headers | ||||||||
|
||||||||
let rec collect_header_lines (ic: in_channel) = | ||||||||
match input_line ic with | ||||||||
| None -> raise (Connection_error "Unexpected end of input") | ||||||||
| Some "\r" -> [] | ||||||||
| Some line -> line :: collect_header_lines ic | ||||||||
|
||||||||
let receive_headers (ic: in_channel) = | ||||||||
let headers = collect_header_lines ic in | ||||||||
List.fold_left parse_header | ||||||||
{ content_length = None; content_type = None } | ||||||||
headers | ||||||||
|
||||||||
let receive_body (ic: in_channel) len = | ||||||||
match really_input_string ic len with | ||||||||
| None -> raise (Connection_error "Unexpected end of input") | ||||||||
| Some body -> body | ||||||||
|
||||||||
let receive_string (ic: in_channel) = | ||||||||
let headers = receive_headers ic in | ||||||||
match headers.content_length with | ||||||||
| None -> raise (Connection_error "Missing Content-Length header") | ||||||||
| Some len -> receive_body ic len | ||||||||
|
||||||||
let output_line (oc: out_channel) message = | ||||||||
let line = message ^ "\r\n" in | ||||||||
output_string oc line | ||||||||
|
||||||||
let send_string (oc: out_channel) message_string = | ||||||||
let length = String.length message_string in | ||||||||
output_line oc ("Content-Length: " ^ string_of_int length); | ||||||||
output_line oc ""; | ||||||||
output_string oc message_string; flush oc | ||||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
(* This file is part of DBL, released under MIT license. | ||
* See LICENSE for details. | ||
*) | ||
|
||
(** Base HTTP-like protocol *) | ||
|
||
(** An exception indicating a problem with the connection, | ||
e.g. unexpected EOF or invalid headers *) | ||
exception Connection_error of string | ||
|
||
(** Receive a message from the client *) | ||
val receive_string : in_channel -> string | ||
|
||
(** Send a message with the specified content to the client *) | ||
val send_string : out_channel -> string -> unit |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
(* This file is part of DBL, released under MIT license. | ||
* See LICENSE for details. | ||
*) | ||
|
||
(** Main loop. Get a message, handle it, send response. *) | ||
|
||
open Connection | ||
open Message | ||
|
||
(* ------------------------------------------------------------------------- *) | ||
(* Auxiliary functions doing type -> json -> string conversion | ||
and sending it to the output *) | ||
|
||
let send_json state json = | ||
let string = Yojson.Safe.to_string json in | ||
send_string (State.out_channel state) string | ||
|
||
let send_response state response = | ||
send_json state (json_of_response response) | ||
|
||
let send_message state message = | ||
send_json state (json_of_message message) | ||
|
||
let send_notification state notification = | ||
let message = message_of_notification notification in | ||
send_message state message | ||
|
||
let send_error state ?id code message = | ||
let error = make_response_error ~code ~message () |> Result.error in | ||
let response = make_response ?id ~result_or_error:error () in | ||
send_response state response | ||
|
||
(* ------------------------------------------------------------------------- *) | ||
(* Main function *) | ||
|
||
let rec run state handle_request handle_notification = | ||
|
||
(* Handle messages in a loop *) | ||
let rec loop state = | ||
let string = receive_string (State.in_channel state) in | ||
match message_of_string string with | ||
(* There was an error parsing the message *) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is it definitely parsing error or could it be some connection error too? If it's the former we could incorporate the comment as self-explanatory custom type: There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's definitely a parsing error (the string is already received). I don't think a custom type is a good idea because the error is produced by the callee and we're just passing it over, so we don't care what kind of error it is. |
||
| Error (code, msg) -> | ||
send_error state code msg; | ||
loop state | ||
(* The message is a notification *) | ||
| Ok ({ id = None; _ } as message) -> begin | ||
match notification_of_message message with | ||
| Ok notification -> | ||
let state = handle_notification state notification in | ||
loop state | ||
(* There was an error interpreting the message as a notification. | ||
Because notifications don't get a response, we ignore it | ||
(according to the specification). *) | ||
| Error _ -> | ||
loop state | ||
end | ||
(* The message is a request *) | ||
| Ok ({ id = Some id; _ } as message) -> begin | ||
match request_of_message message with | ||
| Ok request -> | ||
let state, result = handle_request state request in | ||
let response = make_response ~id ~result_or_error:result () in | ||
send_response state response; | ||
loop state | ||
(* There was an error interpreting the message as a request *) | ||
| Error (code, msg) -> | ||
send_error state ~id code msg; | ||
loop state | ||
end | ||
|
||
in loop state | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
(* This file is part of DBL, released under MIT license. | ||
* See LICENSE for details. | ||
*) | ||
|
||
(** Main loop. Get a message, handle it, send response. *) | ||
|
||
open Message | ||
|
||
(** Send a notification to the client *) | ||
val send_notification : State.t -> server_notification -> unit | ||
|
||
(** Main loop of the server. The fuction expects: | ||
- the initial state | ||
- a handler for requests | ||
- a handler for notifications *) | ||
val run : | ||
State.t -> | ||
(State.t -> request -> State.t * (server_result, response_error) result) -> | ||
(State.t -> client_notification -> State.t) -> | ||
'bottom | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not a fan of mutable state. Do we need to dynamically change this? Perhaps I'll answer this myself as I read further.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think anyone here is. However, without any mutable state we would have to pass a
report
argument to every type-checker function which seems impractical. Regular DBL code has no reason to change this variable and the language server only does it before running the type-checker, so I think there is little risk with using a ref here.