diff --git a/CHANGES.md b/CHANGES.md index ae11470dd..6a08f7796 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,29 @@ +# Unreleased + +## Features + +- Make `code-lens` for nested let bindings configurable (#1567) +- Add support for `.mlx` files, including formatting via `ocamlformat-mlx` and most OCaml LSP features (diagnostics, code actions, hover, etc.) (#1528) + +## Fixes + +- Improve precision of collected metrics timestamps. (#1565) +- Fallback on `.merlin` configuration if no `dune-project` file is found and if + `dot-merlin-reader` is installed. (#1563, fixes #1522) + +# 1.24.0 + +## Features + +- Support for OCaml 5.4 (#1559) + +# 1.23.1 + +## Fixes + +- Fix hover on method calls not showing the type. (#1553, fixes #1552) +- Fix error on opening `.mll` files (#1557) + # 1.21.0 ## Features @@ -21,6 +47,7 @@ - More precise diagnostics in the event of a failed identifier search (`Definition_query`) (#1518) - Remove `ocamlformat` application after `destruct` (that remove some useful parenthesis) (#1519) +- Make compatible with Yojson 3.x by removing `Tuple` and `Variant` (#1534) - Add a new server option `standardHover`, that can be used by clients to disable the default hover provider. When `standardHover = false` diff --git a/dune-project b/dune-project index 6a22c0439..bb25689bf 100644 --- a/dune-project +++ b/dune-project @@ -31,7 +31,7 @@ possible and does not make any assumptions about IO. ") (depends (jsonrpc (= :version)) - (yojson (< 3.0.0)) + yojson (ppx_yojson_conv_lib (>= "v0.14")) (cinaps :with-test) (ppx_expect (and (>= v0.15.0) (< 0.17.0) :with-test)) @@ -45,7 +45,7 @@ possible and does not make any assumptions about IO. (synopsis "LSP Server for OCaml") (description "An LSP server for OCaml.") (depends - (yojson (< 3.0.0)) + yojson (base (>= v0.16.0)) (lsp (= :version)) (jsonrpc (= :version)) @@ -75,8 +75,9 @@ possible and does not make any assumptions about IO. (package (name jsonrpc) - (synopsis "Jsonrpc protocol implemenation") + (synopsis "Jsonrpc protocol implementation") (description "See https://www.jsonrpc.org/specification") (depends + yojson (ocaml (>= 4.08)) (odoc :with-doc))) diff --git a/jsonrpc.opam b/jsonrpc.opam index a0c16048f..dab01279e 100644 --- a/jsonrpc.opam +++ b/jsonrpc.opam @@ -1,6 +1,6 @@ # This file is generated by dune, edit dune-project instead opam-version: "2.0" -synopsis: "Jsonrpc protocol implemenation" +synopsis: "Jsonrpc protocol implementation" description: "See https://www.jsonrpc.org/specification" maintainer: ["Rudi Grinberg "] authors: [ @@ -20,6 +20,7 @@ homepage: "https://github.com/ocaml/ocaml-lsp" bug-reports: "https://github.com/ocaml/ocaml-lsp/issues" depends: [ "dune" {>= "3.0"} + "yojson" "ocaml" {>= "4.08"} "odoc" {with-doc} ] diff --git a/jsonrpc/src/dune b/jsonrpc/src/dune index 4d270c2dd..15f08b825 100644 --- a/jsonrpc/src/dune +++ b/jsonrpc/src/dune @@ -1,4 +1,5 @@ (library (public_name jsonrpc) + (libraries yojson) (instrumentation (backend bisect_ppx))) diff --git a/jsonrpc/src/import.ml b/jsonrpc/src/import.ml index 1f6bd09ee..4caaad9d5 100644 --- a/jsonrpc/src/import.ml +++ b/jsonrpc/src/import.ml @@ -9,18 +9,7 @@ module Option = struct end module Json = struct - type t = - [ `Assoc of (string * t) list - | `Bool of bool - | `Float of float - | `Int of int - | `Intlit of string - | `List of t list - | `Null - | `String of string - | `Tuple of t list - | `Variant of string * t option - ] + type t = Yojson.Safe.t exception Of_json of (string * t) diff --git a/jsonrpc/src/jsonrpc.mli b/jsonrpc/src/jsonrpc.mli index 9b8193772..49effa685 100644 --- a/jsonrpc/src/jsonrpc.mli +++ b/jsonrpc/src/jsonrpc.mli @@ -1,18 +1,7 @@ (** Jsonrpc implementation *) module Json : sig - type t = - [ `Assoc of (string * t) list - | `Bool of bool - | `Float of float - | `Int of int - | `Intlit of string - | `List of t list - | `Null - | `String of string - | `Tuple of t list - | `Variant of string * t option - ] + type t = Yojson.Safe.t (** Raised when conversions from json fail *) exception Of_json of (string * t) diff --git a/lsp-fiber/src/import.ml b/lsp-fiber/src/import.ml index da49c436b..11734fbed 100644 --- a/lsp-fiber/src/import.ml +++ b/lsp-fiber/src/import.ml @@ -64,11 +64,11 @@ module Json = struct | `Bool f -> Bool f | `Assoc o -> Record (List.map o ~f:(fun (k, v) -> k, to_dyn v)) | `List l -> List (List.map l ~f:to_dyn) - | `Tuple args -> Tuple (List.map args ~f:to_dyn) | `Null -> Dyn.Variant ("Null", []) - | `Variant (name, Some arg) -> Variant (name, [ to_dyn arg ]) - | `Variant (name, None) -> Variant (name, []) | `Intlit s -> String s + | _ -> Dyn.Variant ("Unsupported", []) + (* This last case is unused with Yojson >= 3 *) + [@@warning "-11"] ;; end diff --git a/lsp.opam b/lsp.opam index 0cd52e318..f5041c33f 100644 --- a/lsp.opam +++ b/lsp.opam @@ -25,7 +25,7 @@ bug-reports: "https://github.com/ocaml/ocaml-lsp/issues" depends: [ "dune" {>= "3.0"} "jsonrpc" {= version} - "yojson" {< "3.0.0"} + "yojson" "ppx_yojson_conv_lib" {>= "v0.14"} "cinaps" {with-test} "ppx_expect" {>= "v0.15.0" & < "0.17.0" & with-test} diff --git a/lsp/bin/cinaps.ml b/lsp/bin/cinaps.ml index c07c121b5..ea9753d61 100644 --- a/lsp/bin/cinaps.ml +++ b/lsp/bin/cinaps.ml @@ -7,7 +7,7 @@ let preprocess_metamodel = method! or_ path (types : Metamodel.type_ list) = match List.filter_map types ~f:(function - | Literal (Record []) -> None + | Metamodel.Literal (Record []) -> None | _ as t -> Some (self#type_ path t)) with | [] -> assert false @@ -17,10 +17,13 @@ let preprocess_metamodel = | Top (Alias s) when s.name = "TextDocumentContentChangeEvent" -> let t = let union_fields l1 l2 ~f = - let of_map = - String.Map.of_list_map_exn ~f:(fun (x : Metamodel.property) -> x.name, x) + let of_map xs = + List.map xs ~f:(fun (x : Metamodel.property) -> x.name, x) + |> String.Map.of_list in - String.Map.merge (of_map l1) (of_map l2) ~f |> String.Map.values + String.Map.merge (of_map l1) (of_map l2) ~f + |> String.Map.bindings + |> List.map ~f:snd in union_fields f1 f2 ~f:(fun k t1 t2 -> if k = "text" @@ -81,8 +84,9 @@ let expand_superclasses db (m : Metamodel.t) = let structures = let uniquify_fields fields = List.fold_left fields ~init:String.Map.empty ~f:(fun acc (f : Metamodel.property) -> - String.Map.set acc f.name f) - |> String.Map.values + String.Map.add acc ~key:f.name ~data:f) + |> String.Map.bindings + |> List.map ~f:snd in let rec fields_of_type (t : Metamodel.type_) = match t with diff --git a/lsp/bin/dune b/lsp/bin/dune index 6c2b237e6..1284abbaf 100644 --- a/lsp/bin/dune +++ b/lsp/bin/dune @@ -3,7 +3,7 @@ (test (name test_metamodel) (modules test_metamodel) - (libraries stdune yojson lsp_gen) + (libraries yojson lsp_gen) (deps metamodel/metaModel.json) (action (run ./test_metamodel.exe %{deps}))) @@ -13,4 +13,4 @@ (instrumentation (backend bisect_ppx)) (modules :standard \ test_metamodel) - (libraries stdune dyn pp yojson)) + (libraries dyn pp yojson)) diff --git a/lsp/bin/import.ml b/lsp/bin/import.ml index c1b414f8f..d42629d14 100644 --- a/lsp/bin/import.ml +++ b/lsp/bin/import.ml @@ -1,13 +1,70 @@ -include struct - open Stdune - module List = List - module Id = Id - module String = String - module Code_error = Code_error - module Comparable = Comparable - module Top_closure = Top_closure - module Poly = Poly - module Option = Option - - let sprintf = sprintf +let sprintf = Printf.sprintf + +module Option = struct + include Option + + let map t ~f = Option.map f t + + let value_exn = function + | None -> assert false + | Some s -> s + ;; +end + +module List = struct + include ListLabels + + type ('a, 'b) skip_or_either = + | Skip + | Left of 'a + | Right of 'b + + let rev_filter_partition_map = + let rec loop l accl accr ~f = + match l with + | [] -> accl, accr + | x :: l -> + (match f x with + | Skip -> loop l accl accr ~f + | Left y -> loop l (y :: accl) accr ~f + | Right y -> loop l accl (y :: accr) ~f) + in + fun l ~f -> loop l [] [] ~f + ;; + + let filter_partition_map l ~f = + let l, r = rev_filter_partition_map l ~f in + rev l, rev r + ;; +end + +module String = struct + include StringLabels + + let to_dyn = Dyn.string + + module Map = struct + include MoreLabels.Map.Make (String) + + let of_list_reducei xs ~f = + List.fold_left xs ~init:empty ~f:(fun map (k, v) -> + update map ~key:k ~f:(function + | None -> Some v + | Some v' -> Some (f k v v'))) + ;; + + let of_list_map_exn xs ~f = List.map xs ~f |> of_list + let union_exn x y = union ~f:(fun _ _ _ -> assert false) x y + end +end + +module Code_error = struct + let raise name data = + invalid_arg (sprintf "%s %s" name (Dyn.to_string (Dyn.record data))) + ;; +end + +module Poly = struct + let equal = Stdlib.( = ) + let compare = Stdlib.compare end diff --git a/lsp/bin/metamodel/metamodel.ml b/lsp/bin/metamodel/metamodel.ml index 7164d35bc..0fd145b54 100644 --- a/lsp/bin/metamodel/metamodel.ml +++ b/lsp/bin/metamodel/metamodel.ml @@ -1,4 +1,4 @@ -open Stdune +open Import type doc = { since : string option @@ -113,7 +113,7 @@ let fields = function ;; let field ?default (name : string) p fields = - match List.assoc fields name with + match List.assoc_opt name fields with | Some f -> p f | None -> (match default with @@ -122,7 +122,7 @@ let field ?default (name : string) p fields = ;; let field_o name p fields = - match List.assoc fields name with + match List.assoc_opt name fields with | None -> None | Some f -> Some (p f) ;; @@ -137,7 +137,7 @@ let literal lit json = if not (Poly.equal json lit) then error "unexpected liter let enum variants json = match json with | `String s -> - (match List.assoc variants s with + (match List.assoc_opt s variants with | None -> error "not a valid enum value" json | Some v -> v) | _ -> error "not a valid enum value" json @@ -370,7 +370,7 @@ module Entity = struct String.Map.union_exn structures enumerations |> String.Map.union_exn typeAliases ;; - let find t x = String.Map.find_exn t x + let find t x = String.Map.find x t end end diff --git a/lsp/bin/ocaml/json_gen.ml b/lsp/bin/ocaml/json_gen.ml index 44af31da8..7f26ced93 100644 --- a/lsp/bin/ocaml/json_gen.ml +++ b/lsp/bin/ocaml/json_gen.ml @@ -34,7 +34,7 @@ let json_error_pat msg = ;; let is_json_constr (constr : Type.constr) = - List.mem [ "String"; "Int"; "Bool" ] constr.name ~equal:String.equal + List.mem ~set:[ "String"; "Int"; "Bool" ] constr.name ;; module Name = struct diff --git a/lsp/bin/ocaml/ml.ml b/lsp/bin/ocaml/ml.ml index 4973e02e2..7d76e8a9b 100644 --- a/lsp/bin/ocaml/ml.ml +++ b/lsp/bin/ocaml/ml.ml @@ -409,7 +409,7 @@ module Expr = struct let pp_constr f { tag; poly; args } = let tag = - let tag = String.capitalize tag in + let tag = String.capitalize_ascii tag in Pp.verbatim (if poly then "`" ^ tag else tag) in match args with diff --git a/lsp/bin/ocaml/ocaml.ml b/lsp/bin/ocaml/ocaml.ml index fe20955fc..5128925ca 100644 --- a/lsp/bin/ocaml/ocaml.ml +++ b/lsp/bin/ocaml/ocaml.ml @@ -79,9 +79,7 @@ module Expanded = struct | None -> init | Some data -> let new_record = { f with data } in - if List.mem ~equal:Poly.equal init new_record - then init - else new_record :: init) + if List.mem ~set:init new_record then init else new_record :: init) in super#field f ~init end @@ -274,18 +272,18 @@ module Entities = struct type t = (Ident.t * Resolved.t) list let find db e : _ Named.t = - match List.assoc db e with + match List.assoc_opt e db with | Some s -> s | None -> Code_error.raise "Entities.find: unable to find" [ "e", Ident.to_dyn e ] ;; let of_map map ts = - List.map ts ~f:(fun (r : Resolved.t) -> String.Map.find_exn map r.name, r) + List.map ts ~f:(fun (r : Resolved.t) -> String.Map.find r.name map, r) ;; let rev_find (db : t) (resolved : Resolved.t) : Ident.t = match - List.filter_map db ~f:(fun (id, r) -> + List.filter_map db ~f:(fun (id, (r : Resolved.t)) -> if r.name = resolved.name then Some id else None) with | [] -> Code_error.raise "rev_find: resolved not found" [] @@ -327,17 +325,17 @@ end = struct [ Prim.Null; String; Bool; Number; Object; List ] |> List.map ~f:(fun s -> Resolved.Ident s) in - fun set -> List.for_all constrs ~f:(fun e -> List.mem set e ~equal:Poly.equal) + fun set -> List.for_all constrs ~f:(List.mem ~set) ;; let id = Type.name "Jsonrpc.Id.t" let is_same_as_id = - let sort = List.sort ~compare:Poly.compare in + let sort = List.sort ~cmp:Poly.compare in let constrs = [ Prim.String; Number ] |> List.map ~f:(fun s -> Resolved.Ident s) |> sort in - fun cs -> List.equal ( = ) constrs (sort cs) + fun cs -> List.equal ~eq:( = ) constrs (sort cs) ;; (* Any type that includes null needs to be extracted to be converted to an @@ -585,7 +583,7 @@ end = struct let literal_wrapper = match literal_wrapper with | None -> [] - | Some { field_name; literal_value } -> + | Some { Mapper.field_name; literal_value } -> Json_gen.make_literal_wrapper_conv ~field_name ~literal_value @@ -626,7 +624,7 @@ let resolve_typescript (ts : Unresolved.t list) = let db = Entities.of_map db ts in match let idents = new name_idents in - Ident.Top_closure.top_closure + Ident.top_closure ts ~key:(fun x -> Entities.rev_find db x) ~deps:(fun x -> idents#t x ~init:[] |> List.map ~f:(Entities.find db)) @@ -640,7 +638,7 @@ let resolve_typescript (ts : Unresolved.t list) = let of_resolved_typescript db (ts : Resolved.t list) = let simple_enums, everything_else = List.filter_partition_map ts ~f:(fun (t : Resolved.t) -> - if List.mem skipped_ts_decls t.name ~equal:String.equal + if List.mem ~set:skipped_ts_decls t.name then Skip else ( match t.data with @@ -650,7 +648,7 @@ let of_resolved_typescript db (ts : Resolved.t list) = let simple_enums = List.map simple_enums ~f:(fun (t : _ Named.t) -> (* "open" enums need an `Other constructor *) - let allow_other = List.mem ~equal:String.equal with_custom_values t.name in + let allow_other = List.mem ~set:with_custom_values t.name in let data = List.filter_map t.data ~f:(fun (constr, v) -> match (v : Ts_types.Enum.case) with diff --git a/lsp/bin/typescript/ts_types.ml b/lsp/bin/typescript/ts_types.ml index c454410ed..c6206f45c 100644 --- a/lsp/bin/typescript/ts_types.ml +++ b/lsp/bin/typescript/ts_types.ml @@ -262,7 +262,19 @@ module Unresolved = struct end module Ident = struct - module Id = Stdune.Id.Make () + module Id = struct + type t = int + + let counter = ref 0 + + let gen () = + incr counter; + !counter + ;; + + let compare = Int.compare + let to_dyn = Dyn.int + end module T = struct type t = @@ -282,9 +294,44 @@ module Ident = struct let make name = { name; id = Id.gen () } - module C = Comparable.Make (T) - module Set = C.Set - module Top_closure = Top_closure.Make (Set) (Stdune.Monad.Id) + module Keys = struct + include MoreLabels.Set.Make (T) + + let add x y = add y x + let mem x y = mem y x + end + + let top_closure ~key ~deps elements = + let rec loop res visited elt ~temporarily_marked = + let key = key elt in + if Keys.mem temporarily_marked key + then Error [ elt ] + else if not (Keys.mem visited key) + then ( + let visited = Keys.add visited key in + let temporarily_marked = Keys.add temporarily_marked key in + deps elt + |> iter_elts res visited ~temporarily_marked + |> function + | Error l -> Error (elt :: l) + | Ok (res, visited) -> + let res = elt :: res in + Ok (res, visited)) + else Ok (res, visited) + and iter_elts res visited elts ~temporarily_marked = + match elts with + | [] -> Ok (res, visited) + | elt :: elts -> + loop res visited elt ~temporarily_marked + |> (function + | Error _ as result -> result + | Ok (res, visited) -> iter_elts res visited elts ~temporarily_marked) + in + iter_elts [] Keys.empty elements ~temporarily_marked:Keys.empty + |> function + | Ok (res, _visited) -> Ok (List.rev res) + | Error elts -> Error elts + ;; end module Prim = struct @@ -345,15 +392,15 @@ let subst unresolved = method inside s = {} method resolve n = - match String.Map.find params n with + match String.Map.find_opt n params with | Some [] -> assert false | Some (x :: _) -> `Resolved x | None -> - if inside = Some n then `Self else `Unresolved (String.Map.find_exn unresolved n) + if inside = Some n then `Self else `Unresolved (String.Map.find n unresolved) method push x y = let params = - String.Map.update params x ~f:(function + String.Map.update params ~key:x ~f:(function | None -> Some [ y ] | Some [] -> assert false | Some (y' :: xs) -> if y = y' then Some xs else Some (y :: y' :: xs)) @@ -362,9 +409,9 @@ let subst unresolved = method pop x = let params = - String.Map.update params x ~f:(function + String.Map.update params ~key:x ~f:(function | None -> - ignore (String.Map.find_exn params x); + ignore (String.Map.find x params); None | Some [] -> assert false | Some (_ :: xs) -> Some xs) diff --git a/lsp/bin/typescript/ts_types.mli b/lsp/bin/typescript/ts_types.mli index aa33961b9..7f3f72904 100644 --- a/lsp/bin/typescript/ts_types.mli +++ b/lsp/bin/typescript/ts_types.mli @@ -86,7 +86,9 @@ module Unresolved : sig end module Ident : sig - module Id : Id.S + module Id : sig + type t + end type t = { id : Id.t @@ -96,13 +98,11 @@ module Ident : sig val to_dyn : t -> Dyn.t val make : string -> t - module Top_closure : sig - val top_closure - : key:('a -> t) - -> deps:('a -> 'a list) - -> 'a list - -> ('a list, 'a list) result - end + val top_closure + : key:('a -> t) + -> deps:('a -> 'a list) + -> 'a list + -> ('a list, 'a list) result end module Prim : sig diff --git a/lsp/bin/typescript/typescript.ml b/lsp/bin/typescript/typescript.ml index e755d14cd..84ed555fd 100644 --- a/lsp/bin/typescript/typescript.ml +++ b/lsp/bin/typescript/typescript.ml @@ -19,7 +19,7 @@ let name_table (defns : Unresolved.t list) = let resolve_all (defns : Unresolved.t list) = let names = name_table defns in - let defns = String.Map.values names |> List.map ~f:fst in + let defns = String.Map.bindings names |> List.map ~f:snd |> List.map ~f:fst in let names = String.Map.map ~f:snd names in Ts_types.resolve_all defns ~names, names ;; diff --git a/ocaml-lsp-server.opam b/ocaml-lsp-server.opam index 5f1a83a76..08c422c0e 100644 --- a/ocaml-lsp-server.opam +++ b/ocaml-lsp-server.opam @@ -20,7 +20,7 @@ homepage: "https://github.com/ocaml/ocaml-lsp" bug-reports: "https://github.com/ocaml/ocaml-lsp/issues" depends: [ "dune" {>= "3.0"} - "yojson" {< "3.0.0"} + "yojson" "base" {>= "v0.16.0"} "lsp" {= version} "jsonrpc" {= version} diff --git a/ocaml-lsp-server/bin/main.ml b/ocaml-lsp-server/bin/main.ml index 6aef73d00..1be7332d9 100644 --- a/ocaml-lsp-server/bin/main.ml +++ b/ocaml-lsp-server/bin/main.ml @@ -4,20 +4,23 @@ module Cli = Lsp.Cli let () = Printexc.record_backtrace true; let version = ref false in - let read_dot_merlin = ref false in + let prefer_dot_merlin = ref false in let arg = Lsp.Cli.Arg.create () in let spec = [ "--version", Arg.Set version, "print version" ; ( "--fallback-read-dot-merlin" - , Arg.Set read_dot_merlin - , "read Merlin config from .merlin files. The `dot-merlin-reader` package must be \ - installed" ) + , Arg.Set prefer_dot_merlin + , "deprecated, same as --prefer-dot-merlin" ) + ; ( "--prefer-dot-merlin" + , Arg.Set prefer_dot_merlin + , "always read Merlin config from existing .merlin files. The `dot-merlin-reader` \ + package must be installed" ) ] @ Cli.Arg.spec arg in let usage = "ocamllsp [ --stdio | --socket PORT | --port PORT | --pipe PIPE ] [ \ - --clientProcessId pid ]" + --clientProcessId pid ] [ --prefer-dot-merlin ]" in Arg.parse spec (fun _ -> raise @@ Arg.Bad "anonymous arguments aren't allowed") usage; let channel = @@ -37,7 +40,7 @@ let () = let module Exn_with_backtrace = Stdune.Exn_with_backtrace in match Exn_with_backtrace.try_with - (Ocaml_lsp_server.run channel ~read_dot_merlin:!read_dot_merlin) + (Ocaml_lsp_server.run channel ~prefer_dot_merlin:!prefer_dot_merlin) with | Ok () -> () | Error exn -> diff --git a/ocaml-lsp-server/docs/ocamllsp/config.md b/ocaml-lsp-server/docs/ocamllsp/config.md index aea4ddfc3..4eb601516 100644 --- a/ocaml-lsp-server/docs/ocamllsp/config.md +++ b/ocaml-lsp-server/docs/ocamllsp/config.md @@ -15,12 +15,21 @@ interface config { */ extendedHover: { enable : boolean } - /** - * Enable/Disable CodeLens - * @default false - * @since 1.16 - */ - codelens: { enable : boolean } + codelens: { + /** + * Enable/Disable CodeLens + * @default false + * @since 1.16 + */ + enable : boolean, + + /** + * Enable CodeLens for nested let bindings + * @default false + * @since 1.25 + */ + forNestedBindings : boolean + } /** * Enable/Disable Dune diagnostics diff --git a/ocaml-lsp-server/src/code_actions.ml b/ocaml-lsp-server/src/code_actions.ml index 662599eea..3c2eb2f21 100644 --- a/ocaml-lsp-server/src/code_actions.ml +++ b/ocaml-lsp-server/src/code_actions.ml @@ -54,12 +54,10 @@ let compute_ocaml_code_actions (params : CodeActionParams.t) state doc = ] in let batchable, non_batchable = - List.partition_map - ~f:(fun ca -> - match ca.run with - | `Batchable f -> Left f - | `Non_batchable f -> Right f) - enabled_actions + List.partition_map enabled_actions ~f:(fun ca -> + match ca.run with + | `Batchable f -> Base.Either.First f + | `Non_batchable f -> Second f) in let* batch_results = if List.is_empty batchable @@ -125,7 +123,7 @@ let compute server (params : CodeActionParams.t) = (match Document.syntax doc with | Ocamllex | Menhir | Cram | Dune -> Fiber.return (Reply.now (actions (dune_actions @ open_related)), state) - | Ocaml | Reason -> + | Ocaml | Reason | Mlx -> let reply () = let+ code_action_results = compute_ocaml_code_actions params state doc in List.concat [ code_action_results; dune_actions; open_related; merlin_jumps ] diff --git a/ocaml-lsp-server/src/code_actions/action_construct.ml b/ocaml-lsp-server/src/code_actions/action_construct.ml index 5a529543f..3fd5ea045 100644 --- a/ocaml-lsp-server/src/code_actions/action_construct.ml +++ b/ocaml-lsp-server/src/code_actions/action_construct.ml @@ -14,7 +14,7 @@ let code_action pipeline doc (params : CodeActionParams.t) = let src = Document.source doc in Compl.prefix_of_position ~short_path:false src pos in - if not (Merlin_analysis.Typed_hole.can_be_hole prefix) + if not (Typed_hole.can_be_hole prefix) then None else ( let structures = @@ -25,7 +25,7 @@ let code_action pipeline doc (params : CodeActionParams.t) = let pos = Mpipeline.get_lexing_pos pipeline pos in Mbrowse.enclosing pos [ Mbrowse.of_typedtree typedtree ] in - if not (Merlin_analysis.Typed_hole.is_a_hole structures) + if not (Typed_hole.is_a_hole structures) then None else ( (* ocaml-lsp can provide [Construct] values as completion entries, so diff --git a/ocaml-lsp-server/src/compl.ml b/ocaml-lsp-server/src/compl.ml index c5054ff2f..3ef48110e 100644 --- a/ocaml-lsp-server/src/compl.ml +++ b/ocaml-lsp-server/src/compl.ml @@ -315,7 +315,7 @@ let complete let* item = completion_item_capability in item.deprecatedSupport) in - if not (Merlin_analysis.Typed_hole.can_be_hole prefix) + if not (Typed_hole.can_be_hole prefix) then Complete_by_prefix.complete merlin prefix pos ~resolve ~deprecated else ( let reindex_sortText completion_items = diff --git a/ocaml-lsp-server/src/config_data.ml b/ocaml-lsp-server/src/config_data.ml index 3fe22ce36..ccfa0c808 100644 --- a/ocaml-lsp-server/src/config_data.ml +++ b/ocaml-lsp-server/src/config_data.ml @@ -192,70 +192,91 @@ module InlayHints = struct end module Lens = struct - type t = { enable : bool [@default true] } + type t = + { enable : bool [@default true] + ; for_nested_bindings : bool [@key "forNestedBindings"] [@default false] + } [@@deriving_inline yojson] [@@yojson.allow_extra_fields] let _ = fun (_ : t) -> () - let t_of_yojson = - (let _tp_loc = "ocaml-lsp-server/src/config_data.ml.Lens.t" in - function - | `Assoc field_yojsons as yojson -> + +let t_of_yojson = + (let _tp_loc = "ocaml-lsp-server/src/config_data.ml.Lens.t" in + function + | `Assoc field_yojsons as yojson -> let enable_field = ref Ppx_yojson_conv_lib.Option.None + and for_nested_bindings_field = ref Ppx_yojson_conv_lib.Option.None and duplicates = ref [] and extra = ref [] in - let rec iter = function - | (field_name, _field_yojson) :: tail -> - (match field_name with - | "enable" -> - (match Ppx_yojson_conv_lib.( ! ) enable_field with - | Ppx_yojson_conv_lib.Option.None -> - let fvalue = bool_of_yojson _field_yojson in - enable_field := Ppx_yojson_conv_lib.Option.Some fvalue - | Ppx_yojson_conv_lib.Option.Some _ -> - duplicates := field_name :: Ppx_yojson_conv_lib.( ! ) duplicates) - | _ -> ()); - iter tail - | [] -> () - in - iter field_yojsons; - (match Ppx_yojson_conv_lib.( ! ) duplicates with - | _ :: _ -> - Ppx_yojson_conv_lib.Yojson_conv_error.record_duplicate_fields - _tp_loc - (Ppx_yojson_conv_lib.( ! ) duplicates) - yojson - | [] -> - (match Ppx_yojson_conv_lib.( ! ) extra with - | _ :: _ -> - Ppx_yojson_conv_lib.Yojson_conv_error.record_extra_fields - _tp_loc - (Ppx_yojson_conv_lib.( ! ) extra) - yojson - | [] -> - let enable_value = Ppx_yojson_conv_lib.( ! ) enable_field in - { enable = - (match enable_value with - | Ppx_yojson_conv_lib.Option.None -> true - | Ppx_yojson_conv_lib.Option.Some v -> v) - })) - | _ as yojson -> - Ppx_yojson_conv_lib.Yojson_conv_error.record_list_instead_atom _tp_loc yojson - : Ppx_yojson_conv_lib.Yojson.Safe.t -> t) + let rec iter = + function + | (field_name, _field_yojson)::tail -> + ((match field_name with + | "enable" -> + (match Ppx_yojson_conv_lib.(!) enable_field with + | Ppx_yojson_conv_lib.Option.None -> + let fvalue = bool_of_yojson _field_yojson in + enable_field := + (Ppx_yojson_conv_lib.Option.Some fvalue) + | Ppx_yojson_conv_lib.Option.Some _ -> + duplicates := (field_name :: + (Ppx_yojson_conv_lib.(!) duplicates))) + | "forNestedBindings" -> + (match Ppx_yojson_conv_lib.(!) for_nested_bindings_field + with + | Ppx_yojson_conv_lib.Option.None -> + let fvalue = bool_of_yojson _field_yojson in + for_nested_bindings_field := + (Ppx_yojson_conv_lib.Option.Some fvalue) + | Ppx_yojson_conv_lib.Option.Some _ -> + duplicates := (field_name :: + (Ppx_yojson_conv_lib.(!) duplicates))) + | _ -> ()); + iter tail) + | [] -> () in + (iter field_yojsons; + (match Ppx_yojson_conv_lib.(!) duplicates with + | _::_ -> + Ppx_yojson_conv_lib.Yojson_conv_error.record_duplicate_fields + _tp_loc (Ppx_yojson_conv_lib.(!) duplicates) yojson + | [] -> + (match Ppx_yojson_conv_lib.(!) extra with + | _::_ -> + Ppx_yojson_conv_lib.Yojson_conv_error.record_extra_fields + _tp_loc (Ppx_yojson_conv_lib.(!) extra) yojson + | [] -> + let (enable_value, for_nested_bindings_value) = + ((Ppx_yojson_conv_lib.(!) enable_field), + (Ppx_yojson_conv_lib.(!) for_nested_bindings_field)) in + { + enable = + ((match enable_value with + | Ppx_yojson_conv_lib.Option.None -> true + | Ppx_yojson_conv_lib.Option.Some v -> v)); + for_nested_bindings = + ((match for_nested_bindings_value with + | Ppx_yojson_conv_lib.Option.None -> false + | Ppx_yojson_conv_lib.Option.Some v -> v)) + }))) + | _ as yojson -> + Ppx_yojson_conv_lib.Yojson_conv_error.record_list_instead_atom _tp_loc + yojson : Ppx_yojson_conv_lib.Yojson.Safe.t -> t) ;; let _ = t_of_yojson - let yojson_of_t = - (function - | { enable = v_enable } -> + +let yojson_of_t = + (function + | { enable = v_enable; for_nested_bindings = v_for_nested_bindings } -> let bnds : (string * Ppx_yojson_conv_lib.Yojson.Safe.t) list = [] in let bnds = - let arg = yojson_of_bool v_enable in - ("enable", arg) :: bnds - in - `Assoc bnds - : t -> Ppx_yojson_conv_lib.Yojson.Safe.t) + let arg = yojson_of_bool v_for_nested_bindings in + ("forNestedBindings", arg) :: bnds in + let bnds = + let arg = yojson_of_bool v_enable in ("enable", arg) :: bnds in + `Assoc bnds : t -> Ppx_yojson_conv_lib.Yojson.Safe.t) ;; let _ = yojson_of_t @@ -921,7 +942,7 @@ let _ = yojson_of_t [@@@end] let default = - { codelens = Some { enable = false } + { codelens = Some { enable = false; for_nested_bindings = false } ; extended_hover = Some { enable = false } ; standard_hover = Some { enable = true } ; inlay_hints = diff --git a/ocaml-lsp-server/src/document.ml b/ocaml-lsp-server/src/document.ml index ec1ec2956..fd4a64da4 100644 --- a/ocaml-lsp-server/src/document.ml +++ b/ocaml-lsp-server/src/document.ml @@ -8,7 +8,7 @@ module Kind = struct let of_fname_opt p = match Filename.extension p with - | ".ml" | ".eliom" | ".re" -> Some Impl + | ".ml" | ".eliom" | ".re" | ".mll" | ".mly" | ".mlx" -> Some Impl | ".mli" | ".eliomi" | ".rei" -> Some Intf | _ -> None ;; @@ -32,6 +32,7 @@ module Syntax = struct | Menhir | Cram | Dune + | Mlx let human_name = function | Ocaml -> "OCaml" @@ -40,6 +41,7 @@ module Syntax = struct | Menhir -> "Menhir/ocamlyacc" | Cram -> "Cram" | Dune -> "Dune" + | Mlx -> "OCaml.mlx" ;; let all = @@ -52,6 +54,7 @@ module Syntax = struct ; "dune", Dune ; "dune-project", Dune ; "dune-workspace", Dune + ; "ocaml.mlx", Mlx ] ;; @@ -61,6 +64,7 @@ module Syntax = struct | s -> (match Filename.extension s with | ".eliomi" | ".eliom" | ".mli" | ".ml" -> Ok Ocaml + | ".mlx" -> Ok Mlx | ".rei" | ".re" -> Ok Reason | ".mll" -> Ok Ocamllex | ".mly" -> Ok Menhir @@ -156,10 +160,10 @@ end = struct let task = match Lev_fiber.Thread.task t.thread ~f:(fun () -> - let start = Unix.time () in + let start = Unix.gettimeofday () in let pipeline = make_pipeline () in let res = Mpipeline.with_pipeline pipeline (fun () -> f pipeline) in - let stop = Unix.time () in + let stop = Unix.gettimeofday () in res, start, stop) with | Error `Stopped -> assert false @@ -252,7 +256,7 @@ let make wheel config pipeline (doc : DidOpenTextDocumentParams.t) ~position_enc 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 + | Ocaml | Reason | Mlx -> make_merlin wheel config pipeline tdoc syntax | Ocamllex | Menhir | Cram | Dune -> Fiber.return (Other { tdoc; syntax })) ;; @@ -421,8 +425,8 @@ let close t = 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" + let ml, mli, eliom, eliomi, re, rei, mll, mly, mlx = + "ml", "mli", "eliom", "eliomi", "re", "rei", "mll", "mly", "mlx" in let exts_to_switch_to = let kind = @@ -436,13 +440,17 @@ let get_impl_intf_counterparts m uri = in match Syntax.of_fname fname with | Dune | Cram -> [] + | Mlx -> + (match kind with + | Intf -> [ ml; mly; mll; mlx; re ] + | Impl -> [ rei; mli; mly; mll; rei ]) | Ocaml -> (match kind with - | Intf -> [ ml; mly; mll; eliom; re ] + | Intf -> [ ml; mly; mll; eliom; re; mlx ] | Impl -> [ mli; mly; mll; eliomi; rei ]) | Reason -> (match kind with - | Intf -> [ re; ml ] + | Intf -> [ re; ml; mlx ] | Impl -> [ rei; mli ]) | Ocamllex -> [ mli; rei ] | Menhir -> [ mli; rei ] diff --git a/ocaml-lsp-server/src/document.mli b/ocaml-lsp-server/src/document.mli index 735bfd659..6585b6f0b 100644 --- a/ocaml-lsp-server/src/document.mli +++ b/ocaml-lsp-server/src/document.mli @@ -10,6 +10,7 @@ module Syntax : sig | Menhir | Cram | Dune + | Mlx val human_name : t -> string val markdown_name : t -> string diff --git a/ocaml-lsp-server/src/dune.ml b/ocaml-lsp-server/src/dune.ml index 9ebacc37b..e3dbdad2b 100644 --- a/ocaml-lsp-server/src/dune.ml +++ b/ocaml-lsp-server/src/dune.ml @@ -112,10 +112,7 @@ module Poll = | s -> Ok (`Mtime s.st_mtime) ;; - let read_file s = - Fiber.of_thunk (fun () -> - Fiber.return (Result.try_with (fun () -> Io.String_path.read_file s))) - ;; + let read_file s = Fiber.of_thunk (fun () -> Fiber.return (Io.read_file s)) end) type config = diff --git a/ocaml-lsp-server/src/hover_req.ml b/ocaml-lsp-server/src/hover_req.ml index 8a5f932b6..170135475 100644 --- a/ocaml-lsp-server/src/hover_req.ml +++ b/ocaml-lsp-server/src/hover_req.ml @@ -58,8 +58,10 @@ let hover_at_cursor parsetree (`Logical (cursor_line, cursor_col)) = then ( match expr.pexp_desc with | Pexp_constant _ | Pexp_variant _ | Pexp_pack _ -> result := Some `Type_enclosing - | Pexp_ident { loc; _ } | Pexp_construct ({ loc; _ }, _) | Pexp_field (_, { loc; _ }) - -> + | Pexp_ident { loc; _ } + | Pexp_construct ({ loc; _ }, _) + | Pexp_field (_, { loc; _ }) + | Pexp_send (_, { loc; _ }) -> if is_at_cursor loc then result := Some `Type_enclosing else Ast_iterator.default_iterator.expr self expr diff --git a/ocaml-lsp-server/src/import.ml b/ocaml-lsp-server/src/import.ml index 48210f5fd..47daff368 100644 --- a/ocaml-lsp-server/src/import.ml +++ b/ocaml-lsp-server/src/import.ml @@ -9,7 +9,6 @@ include struct module Table = Table module Tuple = Tuple module Unix_env = Env - module Io = Io module Map = Map module Monoid = Monoid module Pid = Pid @@ -18,6 +17,31 @@ include struct let sprintf = sprintf end +module Io = struct + open Base + + let read_file f = + Base.Result.try_with (fun () -> + let fd = Unix.openfile f [ O_CLOEXEC; O_RDONLY ] 0 in + Exn.protect + ~finally:(fun () -> Unix.close fd) + ~f:(fun () -> + match Unix.fstat fd with + | { Unix.st_size; _ } -> + let buf = Bytes.create st_size in + let rec loop pos remains = + if remains > 0 + then ( + let read = Unix.read fd buf pos remains in + if read = 0 + then failwith (sprintf "unable to read all of %s" f) + else loop (pos + read) (remains - read)) + in + loop 0 st_size; + Stdlib.Bytes.unsafe_to_string buf)) + ;; +end + include struct open Base module Queue = Queue @@ -37,13 +61,34 @@ include struct end module List = struct - include Stdune.List - open Base.List + include Base.List + let compare xs ys ~compare = + Base.List.compare (fun x y -> Ordering.to_int (compare x y)) xs ys + ;; + + let sort xs ~compare = sort xs ~compare:(fun x y -> Ordering.to_int (compare x y)) + let fold_left2 xs ys ~init ~f = Stdlib.List.fold_left2 f init xs ys + let assoc xs key = Assoc.find ~equal:Poly.equal xs key + let assoc_opt xs key = assoc xs key + let mem t x ~equal = mem t x ~equal + let map t ~f = map t ~f + let concat_map t ~f = concat_map t ~f + let flatten t = Stdlib.List.flatten t + let filter_map t ~f = filter_map t ~f + let fold_left t ~init ~f = fold_left t ~init ~f let findi xs ~f = findi xs ~f + let find_opt xs ~f = find xs ~f + + let sort_uniq xs ~compare = + Stdlib.List.sort_uniq (fun x y -> Ordering.to_int (compare x y)) xs + ;; + + let for_all xs ~f = for_all xs ~f let find_mapi xs ~f = find_mapi xs ~f let sub xs ~pos ~len = sub xs ~pos ~len let hd_exn t = hd_exn t + let hd_opt t = hd t let nth_exn t n = nth_exn t n let hd t = hd t let filter t ~f = filter t ~f diff --git a/ocaml-lsp-server/src/inference.ml b/ocaml-lsp-server/src/inference.ml index d51401e31..3771fe90f 100644 --- a/ocaml-lsp-server/src/inference.ml +++ b/ocaml-lsp-server/src/inference.ml @@ -71,6 +71,7 @@ let language_id_of_fname s = | ".mli" | ".eliomi" -> "ocaml.interface" | ".ml" | ".eliom" -> "ocaml" | ".rei" | ".re" -> "reason" + | ".mlx" -> "ocaml.mlx" | ".mll" -> "ocaml.ocamllex" | ".mly" -> "ocaml.menhir" | ext -> Code_error.raise "unsupported file extension" [ "extension", String ext ] @@ -79,12 +80,12 @@ let language_id_of_fname s = let open_document_from_file (state : State.t) uri = let filename = Uri.to_path uri in Fiber.of_thunk (fun () -> - match Io.String_path.read_file filename with - | exception Sys_error _ -> + match Io.read_file filename with + | Error _ -> Log.log ~section:"debug" (fun () -> Log.msg "Unable to open file" [ "filename", `String filename ]); Fiber.return None - | text -> + | Ok text -> let languageId = language_id_of_fname filename in let text_document = TextDocumentItem.create ~uri ~languageId ~version:0 ~text in let params = DidOpenTextDocumentParams.create ~textDocument:text_document in diff --git a/ocaml-lsp-server/src/inlay_hints.ml b/ocaml-lsp-server/src/inlay_hints.ml index daf085a39..8e87a309d 100644 --- a/ocaml-lsp-server/src/inlay_hints.ml +++ b/ocaml-lsp-server/src/inlay_hints.ml @@ -29,22 +29,12 @@ let compute (state : State.t) { InlayHintParams.range; textDocument = { uri }; _ c.hint_pattern_variables) |> Option.value ~default:false in - let hint_function_params = - Option.map state.configuration.data.inlay_hints ~f:(fun c -> - c.hint_function_params) - |> Option.value ~default:false - in Document.Merlin.with_pipeline_exn ~name:"inlay-hints" doc (fun pipeline -> let start = range.start |> Position.logical and stop = range.end_ |> Position.logical in let command = Query_protocol.Inlay_hints - ( start - , stop - , hint_let_bindings - , hint_pattern_variables - , hint_function_params - , not inside_test ) + (start, stop, hint_let_bindings, hint_pattern_variables, not inside_test) in let hints = Query_commands.dispatch pipeline command in List.filter_map diff --git a/ocaml-lsp-server/src/merlin_config.ml b/ocaml-lsp-server/src/merlin_config.ml index 50ce15d5d..4e0a55096 100644 --- a/ocaml-lsp-server/src/merlin_config.ml +++ b/ocaml-lsp-server/src/merlin_config.ml @@ -119,7 +119,7 @@ module Dot_protocol_io = let write t x = write t [ x ] end) -let should_read_dot_merlin = ref false +let prefer_dot_merlin = ref false type db = { running : (string, entry) Table.t @@ -299,13 +299,13 @@ let config (t : t) : Mconfig.t Fiber.t = t.entry <- Some entry in let* () = Fiber.return () in - if !should_read_dot_merlin + if !prefer_dot_merlin then Fiber.return (Mconfig.get_external_config t.path t.initial) else ( match find_project_context t.directory with | None -> let+ () = destroy t in - t.initial + Mconfig.get_external_config t.path t.initial | Some (ctx, config_path) -> let* entry = get_process t.db ~dir:ctx.process_dir in let* () = diff --git a/ocaml-lsp-server/src/merlin_config.mli b/ocaml-lsp-server/src/merlin_config.mli index bc057f038..b8431d471 100644 --- a/ocaml-lsp-server/src/merlin_config.mli +++ b/ocaml-lsp-server/src/merlin_config.mli @@ -4,7 +4,7 @@ open Import type t -val should_read_dot_merlin : bool ref +val prefer_dot_merlin : bool ref val config : t -> Mconfig.t Fiber.t val destroy : t -> unit Fiber.t diff --git a/ocaml-lsp-server/src/ocaml_lsp_server.ml b/ocaml-lsp-server/src/ocaml_lsp_server.ml index 3a2ea56f4..b0746ebc5 100644 --- a/ocaml-lsp-server/src/ocaml_lsp_server.ml +++ b/ocaml-lsp-server/src/ocaml_lsp_server.ml @@ -203,7 +203,7 @@ let set_diagnostics detached diagnostics doc = in Diagnostics.set diagnostics (`Merlin (uri, [ no_reason_merlin ])); async (fun () -> Diagnostics.send diagnostics (`One uri)) - | Reason | Ocaml -> + | Reason | Ocaml | Mlx -> async (fun () -> let* () = Diagnostics.merlin_diagnostics diagnostics merlin in Diagnostics.send diagnostics (`One uri))) @@ -363,7 +363,11 @@ module Formatter = struct ;; end -let text_document_lens (state : State.t) { CodeLensParams.textDocument = { uri }; _ } = +let text_document_lens + (state : State.t) + { CodeLensParams.textDocument = { uri }; _ } + ~for_nested_bindings + = let store = state.store in let doc = Document_store.get store uri in match Document.kind doc with @@ -372,7 +376,11 @@ let text_document_lens (state : State.t) { CodeLensParams.textDocument = { uri } | `Merlin doc -> let+ outline = Document.Merlin.dispatch_exn ~name:"outline" doc Outline in let rec symbol_info_of_outline_item (item : Query_protocol.item) = - let children = List.concat_map item.children ~f:symbol_info_of_outline_item in + let children = + if for_nested_bindings + then List.concat_map item.children ~f:symbol_info_of_outline_item + else [] + in match item.outline_type with | None -> children | Some typ -> @@ -496,6 +504,7 @@ let on_request match req with | Client_request.UnknownRequest { meth; params } -> (match + List.assoc [ ( Req_switch_impl_intf.meth , fun ~params state -> Fiber.of_thunk (fun () -> @@ -514,8 +523,7 @@ let on_request , Semantic_highlighting.Debug.on_request_full ) ; ( Req_hover_extended.meth , fun ~params _ -> Req_hover_extended.on_request ~params rpc ) - ] - |> List.assoc_opt meth + ] meth with | None -> Jsonrpc.Response.Error.raise @@ -620,7 +628,8 @@ let on_request | TextDocumentCodeLensResolve codeLens -> now codeLens | TextDocumentCodeLens req -> (match state.configuration.data.codelens with - | Some { enable = true } -> later text_document_lens req + | Some { enable = true; for_nested_bindings } -> + later (text_document_lens ~for_nested_bindings) req | _ -> now []) | TextDocumentHighlight req -> later highlight req | DocumentSymbol { textDocument = { uri }; _ } -> later document_symbol uri @@ -954,10 +963,10 @@ let run_in_directory = fun () -> if Sys.win32 then for_windows else run_in_directory ;; -let run channel ~read_dot_merlin () = +let run channel ~prefer_dot_merlin () = Merlin_utils.Lib_config.set_program_name "ocamllsp"; Merlin_utils.Lib_config.System.set_run_in_directory (run_in_directory ()); - Merlin_config.should_read_dot_merlin := read_dot_merlin; + Merlin_config.prefer_dot_merlin := prefer_dot_merlin; Unix.putenv "__MERLIN_MASTER_PID" (string_of_int (Unix.getpid ())); Lev_fiber.run ~sigpipe:`Ignore (fun () -> let* input, output = stream_of_channel channel in diff --git a/ocaml-lsp-server/src/ocaml_lsp_server.mli b/ocaml-lsp-server/src/ocaml_lsp_server.mli index e74891253..76cc6c224 100644 --- a/ocaml-lsp-server/src/ocaml_lsp_server.mli +++ b/ocaml-lsp-server/src/ocaml_lsp_server.mli @@ -1,4 +1,4 @@ -val run : Lsp.Cli.Channel.t -> read_dot_merlin:bool -> unit -> unit +val run : Lsp.Cli.Channel.t -> prefer_dot_merlin:bool -> unit -> unit module Diagnostics = Diagnostics module Version = Version diff --git a/ocaml-lsp-server/src/ocamlformat.ml b/ocaml-lsp-server/src/ocamlformat.ml index 28437f21f..4c6ce3757 100644 --- a/ocaml-lsp-server/src/ocamlformat.ml +++ b/ocaml-lsp-server/src/ocamlformat.ml @@ -100,9 +100,11 @@ let message = function type formatter = | Reason of Document.Kind.t | Ocaml of Uri.t + | Mlx of Uri.t let args = function | Ocaml uri -> [ sprintf "--name=%s" (Uri.to_path uri); "-" ] + | Mlx uri -> [ "--impl"; sprintf "--name=%s" (Uri.to_path uri); "-" ] | Reason kind -> [ "--parse"; "re"; "--print"; "re" ] @ @@ -114,6 +116,7 @@ let args = function let binary_name t = match t with | Ocaml _ -> "ocamlformat" + | Mlx _ -> "ocamlformat-mlx" | Reason _ -> "refmt" ;; @@ -128,6 +131,7 @@ let formatter doc = match Document.syntax doc with | (Dune | Cram | Ocamllex | Menhir) as s -> Error (Unsupported_syntax s) | Ocaml -> Ok (Ocaml (Document.uri doc)) + | Mlx -> Ok (Mlx (Document.uri doc)) | Reason -> Ok (Reason diff --git a/ocaml-lsp-server/src/ocamlformat.mli b/ocaml-lsp-server/src/ocamlformat.mli index 0d6edc6c1..186f692ba 100644 --- a/ocaml-lsp-server/src/ocamlformat.mli +++ b/ocaml-lsp-server/src/ocamlformat.mli @@ -1,6 +1,7 @@ (** Generic formatting facility for OCaml and Reason sources. - Relies on [ocamlformat] for OCaml and [refmt] for reason *) + Relies on [ocamlformat] for OCaml, [ocamlformat-mlx] for OCaml.mlx, and + [refmt] for Reason. *) open Import diff --git a/ocaml-lsp-server/src/typed_hole.ml b/ocaml-lsp-server/src/typed_hole.ml index f34d39f1f..7062c1893 100644 --- a/ocaml-lsp-server/src/typed_hole.ml +++ b/ocaml-lsp-server/src/typed_hole.ml @@ -1,5 +1,23 @@ open Import +let syntax_repr = "_" +let can_be_hole s = String.equal syntax_repr s + +(* the pattern matching below is taken and modified (minimally, to adapt the + return type) from [Query_commands.dispatch]'s [Construct] branch; + + If we directly dispatched [Construct] command to merlin, we'd be doing + useless computations: we need info whether the expression at the cursor is a + hole, we don't need constructed expressions yet. + + Ideally, merlin should return a callback [option], which is [Some] when the + context is applicable. *) +let is_a_hole = function + | (_, Browse_raw.Module_expr { mod_desc = Tmod_hole; _ }) :: (_, _) :: _ + | (_, Browse_raw.Expression { exp_desc = Texp_hole; _ }) :: _ -> true + | [] | (_, _) :: _ -> false +;; + let in_range range holes = match range with | None -> holes diff --git a/ocaml-lsp-server/src/typed_hole.mli b/ocaml-lsp-server/src/typed_hole.mli index eca4e3b87..03ee8d8ee 100644 --- a/ocaml-lsp-server/src/typed_hole.mli +++ b/ocaml-lsp-server/src/typed_hole.mli @@ -1,3 +1,19 @@ +(** This module should be used to work with typed holes. The main goal is to + hide syntactic representation of a typed hole, which may change in future *) + +(** checks whether the current string matches the syntax representation of a + typed hole *) +val can_be_hole : string -> bool + +(** [is_a_hole nodes] checks whether the leaf node [1] is a typed hole + + Note: this function is extracted from merlin sources handling [Construct] + command in [merlin/src/frontend/query_commands.ml] + + [1] leaf node is the head of the list, as + [Mbrowse.t = (Env.t * Browse_raw.node) list]*) +val is_a_hole : (Ocaml_typing.Env.t * Merlin_specific.Browse_raw.node) list -> bool + val find : range:Range.t option -> position:Position.t diff --git a/ocaml-lsp-server/src/workspace_symbol.ml b/ocaml-lsp-server/src/workspace_symbol.ml index a0251a6b1..55bc91638 100644 --- a/ocaml-lsp-server/src/workspace_symbol.ml +++ b/ocaml-lsp-server/src/workspace_symbol.ml @@ -365,9 +365,7 @@ let run server (state : State.t) (params : WorkspaceSymbolParams.t) = | Error `Cancelled -> assert false | Error (`Exn exn) -> Exn_with_backtrace.reraise exn) in - List.partition_map symbols_results ~f:(function - | Ok r -> Left r - | Error e -> Right e) + List.partition_result symbols_results in let+ () = match errors with diff --git a/ocaml-lsp-server/test/e2e-new/code_lens.ml b/ocaml-lsp-server/test/e2e-new/code_lens.ml new file mode 100644 index 000000000..efac601a1 --- /dev/null +++ b/ocaml-lsp-server/test/e2e-new/code_lens.ml @@ -0,0 +1,117 @@ +open Test.Import + +let change_config client params = Client.notification client (ChangeConfiguration params) + +let codelens client textDocument = + Client.request + client + (TextDocumentCodeLens + { textDocument; workDoneToken = None; partialResultToken = None }) +;; + +let json_of_codelens cs = `List (List.map ~f:CodeLens.yojson_of_t cs) + +let%expect_test "enable codelens for nested let bindings" = + let source = + {ocaml| +let toplevel = "Hello" + +let func x = x + +let f x = + let y = 10 in + let z = 3 in + x + y + z +|ocaml} + in + let req client = + let text_document = TextDocumentIdentifier.create ~uri:Helpers.uri in + let* () = + change_config + client + (DidChangeConfigurationParams.create + ~settings:(`Assoc [ "codelens", `Assoc [ "forNestedBindings", `Bool true ] ])) + in + let* resp_codelens_toplevel = codelens client text_document in + Test.print_result (json_of_codelens resp_codelens_toplevel); + Fiber.return () + in + Helpers.test source req; + [%expect + {| + [ + { + "command": { "command": "", "title": "int -> int" }, + "range": { + "end": { "character": 11, "line": 8 }, + "start": { "character": 0, "line": 5 } + } + }, + { + "command": { "command": "", "title": "int" }, + "range": { + "end": { "character": 12, "line": 6 }, + "start": { "character": 2, "line": 6 } + } + }, + { + "command": { "command": "", "title": "int" }, + "range": { + "end": { "character": 11, "line": 7 }, + "start": { "character": 2, "line": 7 } + } + }, + { + "command": { "command": "", "title": "'a -> 'a" }, + "range": { + "end": { "character": 14, "line": 3 }, + "start": { "character": 0, "line": 3 } + } + }, + { + "command": { "command": "", "title": "string" }, + "range": { + "end": { "character": 22, "line": 1 }, + "start": { "character": 0, "line": 1 } + } + } + ] + |}] +;; + +let%expect_test "enable codelens (default settings disable it for nested let binding)" = + let source = + {ocaml| +let x = + let y = 10 in + "Hello" + +let () = () +|ocaml} + in + let req client = + let text_document = TextDocumentIdentifier.create ~uri:Helpers.uri in + let* () = + change_config + client + (DidChangeConfigurationParams.create + ~settings:(`Assoc [ "codelens", `Assoc [ "enable", `Bool true ] ])) + in + let* resp_codelens_toplevel = codelens client text_document in + Test.print_result (json_of_codelens resp_codelens_toplevel); + Fiber.return () + in + Helpers.test source req; + [%expect + {| + [ + { + "command": { "command": "", "title": "string" }, + "range": { + "end": { "character": 9, "line": 3 }, + "start": { "character": 0, "line": 1 } + } + } + ] + |}] +;; diff --git a/ocaml-lsp-server/test/e2e-new/dune b/ocaml-lsp-server/test/e2e-new/dune index ba58aa87a..a65361f61 100644 --- a/ocaml-lsp-server/test/e2e-new/dune +++ b/ocaml-lsp-server/test/e2e-new/dune @@ -44,6 +44,7 @@ action_inline action_mark_remove code_actions + code_lens completion completions construct @@ -51,6 +52,7 @@ document_flow exit_notification for_ppx + hover hover_extended inlay_hints jump_to_typed_hole diff --git a/ocaml-lsp-server/test/e2e-new/hover.ml b/ocaml-lsp-server/test/e2e-new/hover.ml new file mode 100644 index 000000000..466352e9a --- /dev/null +++ b/ocaml-lsp-server/test/e2e-new/hover.ml @@ -0,0 +1,43 @@ +open Test.Import + +let print_hover hover = + match hover with + | None -> print_endline "no hover response" + | Some hover -> + hover |> Hover.yojson_of_t |> Yojson.Safe.pretty_to_string ~std:false |> print_endline +;; + +let hover client position = + Client.request + client + (TextDocumentHover + { HoverParams.position + ; textDocument = TextDocumentIdentifier.create ~uri:Helpers.uri + ; workDoneToken = None + }) +;; + +let%expect_test "object method call" = + let source = + {ocaml| +let f (o : < g : int -> unit >) = o#g 4 +|ocaml} + in + let position = Position.create ~line:1 ~character:38 in + let req client = + let* resp = hover client position in + let () = print_hover resp in + Fiber.return () + in + Helpers.test source req; + [%expect + {| + { + "contents": { "kind": "plaintext", "value": "int -> unit" }, + "range": { + "end": { "character": 38, "line": 1 }, + "start": { "character": 35, "line": 1 } + } + } + |}] +;;