linol/thirdparty/lsp/ocaml-lsp-server/src/document.ml

473 lines
13 KiB
OCaml

open! Import
open Fiber.O
module Kind = struct
type t =
| Intf
| Impl
let of_fname_opt p =
match Filename.extension p with
| ".ml" | ".eliom" | ".re" -> Some Impl
| ".mli" | ".eliomi" | ".rei" -> Some Intf
| _ -> None
;;
let unsupported uri =
let p = Uri.to_path uri in
Jsonrpc.Response.Error.raise
(Jsonrpc.Response.Error.make
~code:InvalidRequest
~message:"unsupported file extension"
~data:(`Assoc [ "extension", `String (Filename.extension p) ])
())
;;
end
module Syntax = struct
type t =
| Ocaml
| Reason
| Ocamllex
| Menhir
| Cram
| Dune
let human_name = function
| Ocaml -> "OCaml"
| Reason -> "Reason"
| Ocamllex -> "OCamllex"
| Menhir -> "Menhir/ocamlyacc"
| Cram -> "Cram"
| Dune -> "Dune"
;;
let all =
[ "ocaml.interface", Ocaml
; "ocaml", Ocaml
; "reason", Reason
; "ocaml.ocamllex", Ocamllex
; "ocaml.menhir", Menhir
; "cram", Cram
; "dune", Dune
; "dune-project", Dune
; "dune-workspace", Dune
]
;;
let of_fname =
let of_fname_res = function
| "dune" | "dune-workspace" | "dune-project" -> Ok Dune
| s ->
(match Filename.extension s with
| ".eliomi" | ".eliom" | ".mli" | ".ml" -> Ok Ocaml
| ".rei" | ".re" -> Ok Reason
| ".mll" -> Ok Ocamllex
| ".mly" -> Ok Menhir
| ".t" -> Ok Cram
| ext -> Error ext)
in
fun s ->
match of_fname_res s with
| Ok x -> x
| Error ext ->
Jsonrpc.Response.Error.raise
(Jsonrpc.Response.Error.make
~code:InvalidRequest
~message:(Printf.sprintf "unsupported file extension")
~data:(`Assoc [ "extension", `String ext ])
())
;;
let to_language_id x =
List.find_map all ~f:(fun (k, v) -> Option.some_if (v = x) k) |> Option.value_exn
;;
let markdown_name = function
| Ocaml -> "ocaml"
| Reason -> "reason"
| s -> to_language_id s
;;
let of_text_document (td : Text_document.t) =
match List.assoc all (Text_document.languageId td) with
| Some s -> s
| None -> Text_document.documentUri td |> Uri.to_path |> of_fname
;;
end
let await task =
let* cancel_token = Server.cancel_token () in
let f () = Lev_fiber.Thread.await task in
let without_cancellation res =
match res with
| Ok s -> Ok s
| Error (`Exn exn) -> Error exn
| Error `Cancelled ->
let exn = Code_error.E (Code_error.create "unexpected cancellation" []) in
let backtrace = Printexc.get_callstack 10 in
Error { Exn_with_backtrace.exn; backtrace }
in
match cancel_token with
| None -> f () |> Fiber.map ~f:without_cancellation
| Some t ->
let+ res, outcome =
Fiber.Cancel.with_handler t f ~on_cancel:(fun () -> Lev_fiber.Thread.cancel task)
in
(match outcome with
| Not_cancelled -> without_cancellation res
| Cancelled () ->
let e =
Jsonrpc.Response.Error.make ~code:RequestCancelled ~message:"cancelled" ()
in
raise (Jsonrpc.Response.Error.E e))
;;
module Single_pipeline : sig
type t
val create : Lev_fiber.Thread.t -> t
val use
: ?name:string
-> t
-> doc:Text_document.t
-> config:Merlin_config.t
-> f:(Mpipeline.t -> 'a)
-> ('a, Exn_with_backtrace.t) result Fiber.t
val use_with_config
: ?name:string
-> t
-> doc:Text_document.t
-> config:Mconfig.t
-> f:(Mpipeline.t -> 'a)
-> ('a, Exn_with_backtrace.t) result Fiber.t
end = struct
type t = { thread : Lev_fiber.Thread.t } [@@unboxed]
let create thread = { thread }
let use_with_config ?name t ~doc ~config ~f =
let make_pipeline =
let source = Msource.make (Text_document.text doc) in
fun () -> Mpipeline.make config source
in
let task =
match
Lev_fiber.Thread.task t.thread ~f:(fun () ->
let start = Unix.time () in
let pipeline = make_pipeline () in
let res = Mpipeline.with_pipeline pipeline (fun () -> f pipeline) in
let stop = Unix.time () in
res, start, stop)
with
| Error `Stopped -> assert false
| Ok task -> task
in
let* res = await task in
match res with
| Error exn -> Fiber.return (Error exn)
| Ok (res, start, stop) ->
let event =
let module Event = Chrome_trace.Event in
let dur = Event.Timestamp.of_float_seconds (stop -. start) in
let fields =
let name = Option.value name ~default:"unknown" in
Event.common_fields
~cat:[ "merlin" ]
~ts:(Event.Timestamp.of_float_seconds start)
~name
()
in
Event.complete ~dur fields
in
let+ () = Metrics.report event in
Ok res
;;
let use ?name t ~doc ~config ~f =
let* config = Merlin_config.config config in
use_with_config ?name t ~doc ~config ~f
;;
end
type merlin =
{ tdoc : Text_document.t
; pipeline : Single_pipeline.t
; timer : Lev_fiber.Timer.Wheel.task
; merlin_config : Merlin_config.t
; syntax : Syntax.t
; kind : Kind.t option
}
type t =
| Other of
{ tdoc : Text_document.t
; syntax : Syntax.t
}
| Merlin of merlin
let tdoc = function
| Other d -> d.tdoc
| Merlin m -> m.tdoc
;;
let uri t = Text_document.documentUri (tdoc t)
let syntax = function
| Merlin m -> m.syntax
| Other t -> t.syntax
;;
let text t = Text_document.text (tdoc t)
let source t = Msource.make (text t)
let version t = Text_document.version (tdoc t)
let make_merlin wheel merlin_db pipeline tdoc syntax =
let* timer = Lev_fiber.Timer.Wheel.task wheel in
let uri = Text_document.documentUri tdoc in
let path = Uri.to_path uri in
let merlin_config = Merlin_config.DB.get merlin_db uri in
let* mconfig = Merlin_config.config merlin_config in
let kind =
let ext = Filename.extension path in
List.find_map mconfig.merlin.suffixes ~f:(fun (impl, intf) ->
if String.equal ext intf
then Some Kind.Intf
else if String.equal ext impl
then Some Kind.Impl
else None)
in
let kind =
match kind with
| Some _ as k -> k
| None -> Kind.of_fname_opt path
in
Fiber.return (Merlin { merlin_config; tdoc; pipeline; timer; syntax; kind })
;;
let make wheel config pipeline (doc : DidOpenTextDocumentParams.t) ~position_encoding =
Fiber.of_thunk (fun () ->
let tdoc = Text_document.make ~position_encoding doc in
let syntax = Syntax.of_text_document tdoc in
match syntax with
| Ocaml | Reason -> make_merlin wheel config pipeline tdoc syntax
| Ocamllex | Menhir | Cram | Dune -> Fiber.return (Other { tdoc; syntax }))
;;
let update_text ?version t changes =
match Text_document.apply_content_changes ?version (tdoc t) changes with
| exception Text_document.Invalid_utf error ->
Log.log ~section:"warning" (fun () ->
let error =
match error with
| Malformed input ->
[ "message", `String "malformed input"; "input", `String input ]
| Insufficient_input -> [ "message", `String "insufficient input" ]
in
Log.msg
"dropping update due to invalid utf8"
(( "changes"
, Json.yojson_of_list TextDocumentContentChangeEvent.yojson_of_t changes )
:: error));
t
| tdoc ->
(match t with
| Other o -> Other { o with tdoc }
| Merlin t -> Merlin { t with tdoc })
;;
module Merlin = struct
type t = merlin
let to_doc t = Merlin t
let source t = Msource.make (text (Merlin t))
let timer (t : t) = t.timer
let kind t =
match t.kind with
| Some k -> k
| None -> Kind.unsupported (Text_document.documentUri t.tdoc)
;;
let with_pipeline ?name (t : t) f =
Single_pipeline.use ?name t.pipeline ~doc:t.tdoc ~config:t.merlin_config ~f
;;
let with_configurable_pipeline ?name ~config (t : t) f =
Single_pipeline.use_with_config ?name t.pipeline ~doc:t.tdoc ~config ~f
;;
let mconfig (t : t) = Merlin_config.config t.merlin_config
let with_pipeline_exn ?name doc f =
let+ res = with_pipeline ?name doc f in
match res with
| Ok s -> s
| Error exn -> Exn_with_backtrace.reraise exn
;;
let with_configurable_pipeline_exn ?name ~config doc f =
let+ res = with_configurable_pipeline ?name ~config doc f in
match res with
| Ok s -> s
| Error exn -> Exn_with_backtrace.reraise exn
;;
let dispatch ?name t command =
with_pipeline ?name t (fun pipeline -> Query_commands.dispatch pipeline command)
;;
let dispatch_exn ?name t command =
with_pipeline_exn ?name t (fun pipeline -> Query_commands.dispatch pipeline command)
;;
let doc_comment pipeline pos =
let res =
let command = Query_protocol.Document (None, pos) in
Query_commands.dispatch pipeline command
in
match res with
| `Found s | `Builtin s -> Some s
| _ -> None
;;
let syntax_doc pipeline pos =
let res =
let command = Query_protocol.Syntax_document pos in
Query_commands.dispatch pipeline command
in
match res with
| `Found s -> Some s
| `No_documentation -> None
;;
type type_enclosing =
{ loc : Loc.t
; typ : string
; doc : string option
; syntax_doc : Query_protocol.syntax_doc_result option
}
let type_enclosing ?name doc pos verbosity ~with_syntax_doc =
with_pipeline_exn ?name doc (fun pipeline ->
let command = Query_protocol.Type_enclosing (None, pos, Some 0) in
let pipeline =
match verbosity with
| 0 -> pipeline
| verbosity ->
let source = source doc in
let config = Mpipeline.final_config pipeline in
let config =
{ config with query = { config.query with verbosity = Lvl verbosity } }
in
Mpipeline.make config source
in
let res = Query_commands.dispatch pipeline command in
match res with
| [] | (_, `Index _, _) :: _ -> None
| (loc, `String typ, _) :: _ ->
let doc = doc_comment pipeline pos in
let syntax_doc =
match with_syntax_doc with
| true -> syntax_doc pipeline pos
| false -> None
in
Some { loc; typ; doc; syntax_doc })
;;
let doc_comment ?name doc pos =
with_pipeline_exn ?name doc (fun pipeline -> doc_comment pipeline pos)
;;
end
let edit t text_edits =
let version = version t in
let textDocument =
OptionalVersionedTextDocumentIdentifier.create ~uri:(uri t) ~version ()
in
let edit =
TextDocumentEdit.create
~textDocument
~edits:(List.map text_edits ~f:(fun text_edit -> `TextEdit text_edit))
in
WorkspaceEdit.create ~documentChanges:[ `TextDocumentEdit edit ] ()
;;
let kind = function
| Merlin merlin -> `Merlin merlin
| Other _ -> `Other
;;
let merlin_exn t =
match kind t with
| `Merlin m -> m
| `Other ->
Code_error.raise
"Document.merlin_exn"
[ "t", Dyn.string @@ DocumentUri.to_string @@ uri t ]
;;
let close t =
match t with
| Other _ -> Fiber.return ()
| Merlin t ->
Fiber.fork_and_join_unit
(fun () -> Merlin_config.destroy t.merlin_config)
(fun () -> Lev_fiber.Timer.Wheel.cancel t.timer)
;;
let get_impl_intf_counterparts m uri =
let fpath = Uri.to_path uri in
let fname = Filename.basename fpath in
let ml, mli, eliom, eliomi, re, rei, mll, mly =
"ml", "mli", "eliom", "eliomi", "re", "rei", "mll", "mly"
in
let exts_to_switch_to =
let kind =
match m with
| Some m -> Merlin.kind m
| None ->
(* still try to guess the kind *)
(match Kind.of_fname_opt fpath with
| Some k -> k
| None -> Kind.unsupported uri)
in
match Syntax.of_fname fname with
| Dune | Cram -> []
| Ocaml ->
(match kind with
| Intf -> [ ml; mly; mll; eliom; re ]
| Impl -> [ mli; mly; mll; eliomi; rei ])
| Reason ->
(match kind with
| Intf -> [ re; ml ]
| Impl -> [ rei; mli ])
| Ocamllex -> [ mli; rei ]
| Menhir -> [ mli; rei ]
in
let fpath_w_ext ext = Filename.remove_extension fpath ^ "." ^ ext in
let find_switch exts =
List.filter_map exts ~f:(fun ext ->
let file_to_switch_to = fpath_w_ext ext in
Option.some_if (Sys.file_exists file_to_switch_to) file_to_switch_to)
in
let files_to_switch_to =
match find_switch exts_to_switch_to with
| [] ->
let switch_to_ext = List.hd_exn exts_to_switch_to in
let switch_to_fpath = fpath_w_ext switch_to_ext in
[ switch_to_fpath ]
| to_switch_to -> to_switch_to
in
List.map ~f:Uri.of_path files_to_switch_to
;;
let substring doc range =
let start, end_ = Text_document.absolute_range (tdoc doc) range in
let text = text doc in
if start < 0 || start > end_ || end_ > String.length text
then None
else Some (String.sub text ~pos:start ~len:(end_ - start))
;;