Skip to content

Commit

Permalink
lsp: initial structure (#195)
Browse files Browse the repository at this point in the history
* lsp: initial structure

* lsp: error handling
  • Loading branch information
EduardoRFS authored Nov 28, 2023
1 parent 19495ea commit 708513f
Show file tree
Hide file tree
Showing 15 changed files with 548 additions and 0 deletions.
5 changes: 5 additions & 0 deletions teikalsp/dune
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
(executable
(name teikalsp)
(libraries lsp eio eio_main)
(preprocess
(pps ppx_deriving.show ppx_deriving.eq ppx_deriving.ord sedlex.ppx)))
178 changes: 178 additions & 0 deletions teikalsp/lsp_channel.ml
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
module Io : sig
type 'a t

val return : 'a -> 'a t
val raise : exn -> 'a t
val await : 'a t -> 'a
val async : (sw:Eio.Switch.t -> ('a, exn) result) -> 'a t

module O : sig
val ( let+ ) : 'a t -> ('a -> 'b) -> 'b t
val ( let* ) : 'a t -> ('a -> 'b t) -> 'b t
end
end = struct
type 'a t = sw:Eio.Switch.t -> ('a, exn) result Eio.Promise.t

let await t = Eio.Switch.run @@ fun sw -> Eio.Promise.await_exn (t ~sw)
let return value ~sw:_ = Eio.Promise.create_resolved (Ok value)
let error desc ~sw:_ = Eio.Promise.create_resolved (Error desc)

let async f ~sw =
let promise, resolver = Eio.Promise.create () in
( Eio.Fiber.fork ~sw @@ fun () ->
try
let result = f ~sw in
Eio.Promise.resolve resolver result
with exn -> Eio.Promise.resolve resolver @@ Error exn );
promise

let bind t f =
async @@ fun ~sw ->
match Eio.Promise.await (t ~sw) with
| Ok value -> Eio.Promise.await @@ f value ~sw
| Error desc -> Error desc

let raise = error

module O = struct
let ( let+ ) x f = bind x @@ fun value -> return @@ f value
let ( let* ) = bind
end
end

module Chan : sig
type input
type output

(* eio *)
val of_source : #Eio.Flow.source -> input
val with_sink : #Eio.Flow.sink -> (output -> 'a) -> 'a

(* lsp *)
val read_line : input -> string option Io.t
val read_exactly : input -> int -> string option Io.t
val write : output -> string -> unit Io.t
end = struct
type input = Input of { mutex : Eio.Mutex.t; buf : Eio.Buf_read.t }
type output = Output of { mutex : Eio.Mutex.t; buf : Eio.Buf_write.t }

(* TODO: magic numbers *)
let initial_size = 1024
let max_size = 1024 * 1024

let of_source source =
let mutex = Eio.Mutex.create () in
let buf = Eio.Buf_read.of_flow ~initial_size ~max_size source in
Input { mutex; buf }

let with_sink sink f =
let mutex = Eio.Mutex.create () in
Eio.Buf_write.with_flow ~initial_size sink @@ fun buf ->
f @@ Output { mutex; buf }

let read_line input =
let (Input { mutex; buf }) = input in
Io.async @@ fun ~sw:_ ->
(* TODO: what this protect does? *)
Eio.Mutex.use_rw ~protect:true mutex @@ fun () ->
match Eio.Buf_read.eof_seen buf with
| true -> Ok None
| false -> Ok (Some (Eio.Buf_read.line buf))

let read_exactly input size =
let (Input { mutex; buf }) = input in
Io.async @@ fun ~sw:_ ->
Eio.Mutex.use_rw ~protect:true mutex @@ fun () ->
match Eio.Buf_read.eof_seen buf with
| true -> Ok None
| false -> Ok (Some (Eio.Buf_read.take size buf))

let write output str =
let (Output { mutex; buf }) = output in
Io.async @@ fun ~sw:_ ->
Eio.Mutex.use_rw ~protect:true mutex @@ fun () ->
Ok (Eio.Buf_write.string buf str)
end

module Lsp_io = Lsp.Io.Make (Io) (Chan)
open Jsonrpc
open Lsp_error

(* TODO: is a mutex needed for write? *)
type channel = Chan.output
type t = channel

let notify channel notification =
(* TODO: fork here *)
(* TODO: buffering and async? *)
let notification = Lsp.Server_notification.to_jsonrpc notification in
Io.await @@ Lsp_io.write channel @@ Notification notification

let respond channel response =
Io.await @@ Lsp_io.write channel @@ Response response

let rec input_loop ~input ~output with_ =
(* TODO: buffering and async handling *)
match Io.await @@ Lsp_io.read input with
| Some packet ->
let () = with_ packet in
input_loop ~input ~output with_
| exception exn -> (* TODO: handle this exception *) raise exn
| None ->
(* TODO: this means EOF right? *)
()

let request_of_jsonrpc request =
match Lsp.Client_request.of_jsonrpc request with
| Ok request -> request
| Error error -> fail (Error_invalid_notification { error })

let notification_of_jsonrpc notification =
match Lsp.Client_notification.of_jsonrpc notification with
| Ok notification -> notification
| Error error -> fail (Error_invalid_notification { error })

type on_request = {
f :
'response.
channel ->
'response Lsp.Client_request.t ->
('response, Response.Error.t) result;
}

let listen ~input ~output ~on_request ~on_notification =
let on_request channel request =
(* TODO: error handling *)
let result =
let (E request) = request_of_jsonrpc request in
match on_request.f channel request with
| Ok result -> Ok (Lsp.Client_request.yojson_of_result request result)
| Error _error as error -> error
in
let response = Jsonrpc.Response.{ id = request.id; result } in
respond channel response
in
let on_notification channel notification =
let notification = notification_of_jsonrpc notification in
on_notification channel notification
in

let input = Chan.of_source input in
Chan.with_sink output @@ fun channel ->
input_loop ~input ~output @@ fun packet ->
(* TODO: make this async? *)
match packet with
| Notification notification -> on_notification channel notification
| Request request -> on_request channel request
| Batch_call calls ->
(* TODO: what if one fails? It should not prevents the others *)
List.iter
(fun call ->
match call with
| `Request request -> on_request channel request
| `Notification notification -> on_notification channel notification)
calls
(* TODO: can the server receive a response?
Yes but right now it will not be supported *)
| Response _ -> fail Error_response_unsupported
| Batch_response _ -> fail Error_response_unsupported
25 changes: 25 additions & 0 deletions teikalsp/lsp_channel.mli
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
open Jsonrpc

type channel
type t = channel

val notify : channel -> Lsp.Server_notification.t -> unit

type on_request = {
f :
'response.
channel ->
'response Lsp.Client_request.t ->
('response, Response.Error.t) result;
}

(* TODO: request*)
val listen :
input:#Eio.Flow.source ->
output:#Eio.Flow.sink ->
on_request:on_request ->
on_notification:(channel -> Lsp.Client_notification.t -> unit) ->
unit

(* val input_loop : input:Chan.input ->
output:Chan.output -> (Jsonrpc.Packet.t -> Jsonrpc.Packet.t list) -> unit) *)
52 changes: 52 additions & 0 deletions teikalsp/lsp_context.ml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
open Lsp.Types
open Lsp_error
module Document_uri_map = Map.Make (DocumentUri)

(* TODO: capabilities *)
(* TODO: initialized *)
type status = Handshake | Running

type context = {
mutable status : status;
mutable text_documents : Lsp_text_document.t Document_uri_map.t;
}

type t = context

let create () = { status = Handshake; text_documents = Document_uri_map.empty }
let status context = context.status

let initialize context =
match context.status with
| Handshake -> context.status <- Running
| Running -> fail Error_invalid_status_during_initialize

let update_text_documents context f =
let text_documents = context.text_documents in
let text_documents = f text_documents in
context.text_documents <- text_documents

let open_text_document context uri text_document =
update_text_documents context @@ fun text_documents ->
(match Document_uri_map.mem uri text_documents with
| true -> fail Error_text_document_already_in_context
| false -> ());
Document_uri_map.add uri text_document text_documents

let change_text_document context uri cb =
update_text_documents context @@ fun text_documents ->
let text_document =
match Document_uri_map.find_opt uri text_documents with
| Some text_document -> text_document
| None -> fail Error_text_document_not_in_context
in
let text_document = cb text_document in
(* TODO: only accept if version is newer or equal *)
Document_uri_map.add uri text_document text_documents

let close_text_document context uri =
update_text_documents context @@ fun text_documents ->
(match Document_uri_map.mem uri text_documents with
| true -> ()
| false -> fail Error_text_document_not_in_context);
Document_uri_map.remove uri text_documents
21 changes: 21 additions & 0 deletions teikalsp/lsp_context.mli
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
open Lsp.Types

type status = private Handshake | Running
type context
type t = context

(* TODO: rollback? Requests and notifications should probably be atomic *)
val create : unit -> context
val status : context -> status
val initialize : context -> unit

(* documents *)
val open_text_document : context -> DocumentUri.t -> Lsp_text_document.t -> unit

val change_text_document :
context ->
DocumentUri.t ->
(Lsp_text_document.t -> Lsp_text_document.t) ->
unit

val close_text_document : context -> DocumentUri.t -> unit
34 changes: 34 additions & 0 deletions teikalsp/lsp_error.ml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
open Lsp.Types

type error =
(* channel *)
| Error_request_unsupported
| Error_response_unsupported
| Error_invalid_request of { error : string }
| Error_invalid_notification of { error : string }
(* server *)
| Error_unsupported_request
| Error_unsupported_notification
(* context *)
| Error_notification_before_initialize
| Error_invalid_status_during_initialize
| Error_text_document_already_in_context
| Error_text_document_not_in_context
(* notification *)
| Error_multiple_content_changes of {
content_changes : TextDocumentContentChangeEvent.t list; [@opaque]
}
| Error_partial_content_change of {
content_change : TextDocumentContentChangeEvent.t; [@opaque]
}
| Error_invalid_content_change of {
content_change : TextDocumentContentChangeEvent.t; [@opaque]
}
| Error_unknown_language_id of { language_id : string }

and t = error [@@deriving show]

(* TODO: what happen with errors? *)
exception Lsp_error of { error : error }

let fail error = raise (Lsp_error { error })
33 changes: 33 additions & 0 deletions teikalsp/lsp_error.mli
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
open Lsp.Types

type error =
(* channel *)
| Error_request_unsupported
| Error_response_unsupported
| Error_invalid_request of { error : string }
| Error_invalid_notification of { error : string }
(* server *)
| Error_unsupported_request
| Error_unsupported_notification
(* context *)
| Error_notification_before_initialize
| Error_invalid_status_during_initialize
| Error_text_document_already_in_context
| Error_text_document_not_in_context
(* notification *)
| Error_multiple_content_changes of {
content_changes : TextDocumentContentChangeEvent.t list;
}
| Error_partial_content_change of {
content_change : TextDocumentContentChangeEvent.t;
}
| Error_invalid_content_change of {
content_change : TextDocumentContentChangeEvent.t;
}
| Error_unknown_language_id of { language_id : string }

type t = error [@@deriving show]

exception Lsp_error of { error : error }

val fail : error -> 'a
Loading

0 comments on commit 708513f

Please sign in to comment.