linol/thirdparty/lsp/ocaml-lsp-server/test/e2e-new/test.ml

256 lines
7.4 KiB
OCaml

module Import = struct
include struct
include Stdune
module List = struct
include List
let find_mapi ~f l =
let rec k i = function
| [] -> None
| x :: xs ->
(match f i x with
| Some x' -> Some x'
| None -> (k [@tailcall]) (i + 1) xs)
in
k 0 l
;;
let take n l =
let rec take acc n l =
if n = 0
then acc
else (
match l with
| [] -> failwith "list shorter than n"
| x :: xs -> (take [@tailcall]) (x :: acc) (n - 1) xs)
in
List.rev (take [] n l)
;;
end
module Array = struct
include Array
module Iter : sig
type 'a t
val create : 'a array -> 'a t
val has_next : 'a t -> bool
val next : 'a t -> 'a option
val next_exn : 'a t -> 'a
end = struct
type 'a t =
{ contents : 'a array
; mutable ix : int
}
let create contents = { contents; ix = 0 }
let has_next t = t.ix < Array.length t.contents
let next_exn t =
let { contents; ix } = t in
let v = contents.(ix) in
t.ix <- ix + 1;
v
;;
let next t = if has_next t then Some (next_exn t) else None
end
end
end
include Fiber.O
module Client = Lsp_fiber.Client
include Lsp.Types
module Uri = Lsp.Uri
module Position = Ocaml_lsp_server.Position
end
open Import
module T : sig
val run_with_status
: ?extra_env:string list
-> ?handler:unit Client.Handler.t
-> (unit Client.t -> 'a Fiber.t)
-> Unix.process_status * 'a
val run
: ?extra_env:string list
-> ?handler:unit Client.Handler.t
-> (unit Client.t -> 'a Fiber.t)
-> 'a
end = struct
let _PATH = Bin.parse_path (Option.value ~default:"" @@ Env.get Env.initial "PATH")
let bin = Bin.which "ocamllsp" ~path:_PATH |> Option.value_exn |> Path.to_string
let run_with_status ?(extra_env = []) ?handler f =
let stdin_i, stdin_o = Unix.pipe ~cloexec:true () in
let stdout_i, stdout_o = Unix.pipe ~cloexec:true () in
let pid =
let env =
let current = Unix.environment () in
Array.to_list current @ extra_env |> Spawn.Env.of_list
in
Spawn.spawn ~env ~prog:bin ~argv:[ bin ] ~stdin:stdin_i ~stdout:stdout_o ()
in
Unix.close stdin_i;
Unix.close stdout_o;
let handler =
match handler with
| Some h -> h
| None -> Client.Handler.make ()
in
let init =
let blockity =
if Sys.win32
then `Blocking
else (
Unix.set_nonblock stdout_i;
Unix.set_nonblock stdin_o;
`Non_blocking true)
in
let make fd what =
let fd = Lev_fiber.Fd.create fd blockity in
Lev_fiber.Io.create fd what
in
let* in_ = make stdout_i Input in
let* out = make stdin_o Output in
let io = Lsp_fiber.Fiber_io.make in_ out in
let client = Client.make handler io () in
f client
in
(* TODO replace the wheel once we can cancel sleep *)
let waitpid wheel =
let* timeout = Lev_fiber.Timer.Wheel.task wheel in
Fiber.finalize ~finally:(fun () -> Lev_fiber.Timer.Wheel.stop wheel)
@@ fun () ->
let cancelled = ref false in
Fiber.fork_and_join_unit
(fun () ->
Lev_fiber.Timer.Wheel.await timeout
>>| function
| `Cancelled -> ()
| `Ok ->
Unix.kill pid Sys.sigkill;
cancelled := true)
(fun () ->
let* (server_exit_status : Unix.process_status) = Lev_fiber.waitpid ~pid in
let+ () =
if !cancelled then Fiber.return () else Lev_fiber.Timer.Wheel.cancel timeout
in
server_exit_status)
in
Lev_fiber.run (fun () ->
let* wheel = Lev_fiber.Timer.Wheel.create ~delay:3.0 in
let+ res = init
and+ status =
Fiber.fork_and_join_unit
(fun () -> Lev_fiber.Timer.Wheel.run wheel)
(fun () -> waitpid wheel)
in
status, res)
|> Lev_fiber.Error.ok_exn
;;
let run ?extra_env ?handler f = snd @@ run_with_status ?extra_env ?handler f
end
include T
let drain_diagnostics () =
let diagnostics = Fiber.Ivar.create () in
let on_notification _ = function
| Lsp.Server_notification.PublishDiagnostics _ ->
let* diag = Fiber.Ivar.peek diagnostics in
(match diag with
| Some _ -> Fiber.return ()
| None -> Fiber.Ivar.fill diagnostics ())
| _ -> Fiber.return ()
in
on_notification, diagnostics
;;
let run_request ?(prep = fun _ -> Fiber.return ()) ?settings request =
let on_notification, diagnostics = drain_diagnostics () in
let handler = Client.Handler.make ~on_notification () in
run ~handler
@@ fun client ->
let run_client () =
let capabilities =
let window =
let showDocument = ShowDocumentClientCapabilities.create ~support:true in
WindowClientCapabilities.create ~showDocument ()
in
ClientCapabilities.create ~window ()
in
Client.start client (InitializeParams.create ~capabilities ())
in
let run =
let* (_ : InitializeResult.t) = Client.initialized client in
let* () = prep client in
let* () =
match settings with
| Some settings -> Client.notification client (ChangeConfiguration { settings })
| None -> Fiber.return ()
in
Client.request client request
in
Fiber.fork_and_join_unit run_client (fun () ->
let* ret = run in
let* () = Fiber.Ivar.read diagnostics in
let+ () = Client.stop client in
ret)
;;
let openDocument ~client ~uri ~source =
let textDocument =
TextDocumentItem.create ~uri ~languageId:"ocaml" ~version:0 ~text:source
in
Client.notification
client
(TextDocumentDidOpen (DidOpenTextDocumentParams.create ~textDocument))
;;
let offset_of_position src (pos : Position.t) =
let line_offset =
String.split_lines src
|> List.take pos.line
|> List.fold_left ~init:0 ~f:(fun s l -> s + String.length l)
in
line_offset + pos.line (* account for line endings *) + pos.character
;;
let apply_edits src edits =
let edits =
List.sort edits ~compare:(fun (e : TextEdit.t) (e' : TextEdit.t) ->
Position.compare e.range.start e'.range.start)
in
(* check that edits are non-overlapping *)
let rec overlaps : TextEdit.t list -> _ = function
| [] | [ _ ] -> false
| e :: e' :: es ->
(match Position.compare e.range.end_ e'.range.start with
| Gt -> true
| Lt | Eq -> overlaps (e' :: es))
in
if overlaps edits then failwith "overlapping edits";
let _, edits =
(* compute start and end character offsets for each edit *)
List.map edits ~f:(fun (e : TextEdit.t) ->
e.newText, offset_of_position src e.range.start, offset_of_position src e.range.end_)
(* update the offsets to account for preceding edits *)
|> List.fold_left_map ~init:0 ~f:(fun offset (new_text, start, end_) ->
if end_ < start then failwith "invalid edit: end before start";
( offset + (String.length new_text - (end_ - start))
, (new_text, start + offset, end_ + offset) ))
in
(* apply edits *)
List.fold_left edits ~init:src ~f:(fun src (new_text, start, end_) ->
String.take src start ^ new_text ^ String.drop src end_)
;;
let print_result result =
result |> Yojson.Safe.pretty_to_string ~std:false |> print_endline
;;