diff --git a/CHANGES.md b/CHANGES.md index e8250c826..962517076 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,6 +2,8 @@ ## Features +- Code action for inlining let bindings within a module or expression. (#847) + - Tag "unused code" and "deprecated" warnings, allowing clients to better display them. (#848) diff --git a/ocaml-lsp-server/src/code_actions.ml b/ocaml-lsp-server/src/code_actions.ml index 2b5785ce3..fb834270c 100644 --- a/ocaml-lsp-server/src/code_actions.ml +++ b/ocaml-lsp-server/src/code_actions.ml @@ -90,6 +90,7 @@ let compute server (params : CodeActionParams.t) = ; Action_add_rec.t ; Action_mark_remove_unused.mark ; Action_mark_remove_unused.remove + ; Action_inline.t ] in List.concat diff --git a/ocaml-lsp-server/src/code_actions/action_inline.ml b/ocaml-lsp-server/src/code_actions/action_inline.ml new file mode 100644 index 000000000..68daf8b70 --- /dev/null +++ b/ocaml-lsp-server/src/code_actions/action_inline.ml @@ -0,0 +1,455 @@ +open Import +module H = Ocaml_parsing.Ast_helper + +let action_title = "Inline into uses" + +type inline_task = + { inlined_var : Ident.t + ; inlined_expr : Typedtree.expression (** the expression to inline *) + } + +let find_path_by_name id env = + try Some (fst (Ocaml_typing.Env.find_value_by_name id env)) + with Not_found -> None + +let check_shadowing (inlined_expr : Typedtree.expression) new_env = + let module I = Ocaml_typing.Tast_iterator in + let orig_env = inlined_expr.exp_env in + let exception Env_mismatch of (Longident.t * [ `Unbound | `Shadowed ]) in + let expr_iter (iter : I.iterator) (expr : Typedtree.expression) = + match expr.exp_desc with + | Texp_ident (path, { txt = ident; _ }, _) -> ( + let in_orig_env = + find_path_by_name ident orig_env + |> Option.map ~f:(Path.same path) + |> Option.value ~default:false + in + if in_orig_env then + match find_path_by_name ident new_env with + | Some path' -> + if not (Path.same path path') then + raise_notrace (Env_mismatch (ident, `Shadowed)) + | None -> raise_notrace (Env_mismatch (ident, `Unbound))) + | _ -> I.default_iterator.expr iter expr + in + let iter = { I.default_iterator with expr = expr_iter } in + try + iter.expr iter inlined_expr; + Ok () + with Env_mismatch m -> Error m + +let string_of_error (ident, reason) = + let reason = + match reason with + | `Unbound -> "unbound" + | `Shadowed -> "shadowed" + in + Format.asprintf + "'%a' is %s in inlining context" + Pprintast.longident + ident + reason + +let contains loc pos = + match Position.compare_inclusion pos (Range.of_loc loc) with + | `Outside _ -> false + | `Inside -> true + +let find_inline_task typedtree pos = + let exception Found of inline_task in + let module I = Ocaml_typing.Tast_iterator in + let expr_iter (iter : I.iterator) (expr : Typedtree.expression) = + if contains expr.exp_loc pos then + match expr.exp_desc with + | Texp_let + ( Nonrecursive + , [ { vb_pat = { pat_desc = Tpat_var (inlined_var, { loc; _ }); _ } + ; vb_expr = inlined_expr + ; _ + } + ] + , _ ) + when contains loc pos -> + raise_notrace (Found { inlined_var; inlined_expr }) + | _ -> I.default_iterator.expr iter expr + in + let structure_item_iter (iter : I.iterator) (item : Typedtree.structure_item) + = + if contains item.str_loc pos then + match item.str_desc with + | Tstr_value + ( Nonrecursive + , [ { vb_pat = { pat_desc = Tpat_var (inlined_var, { loc; _ }); _ } + ; vb_expr = inlined_expr + ; _ + } + ] ) + when contains loc pos -> + raise_notrace (Found { inlined_var; inlined_expr }) + | _ -> I.default_iterator.structure_item iter item + in + let iterator = + { I.default_iterator with + expr = expr_iter + ; structure_item = structure_item_iter + } + in + try + iterator.structure iterator typedtree; + None + with Found task -> Some task + +(** [find_parsetree_loc pl loc] finds an expression node in the parsetree with + location [loc] *) +let find_parsetree_loc pipeline loc = + let exception Found of Parsetree.expression in + try + let expr_iter (iter : Ast_iterator.iterator) (expr : Parsetree.expression) = + if Loc.compare expr.pexp_loc loc = 0 then raise_notrace (Found expr) + else Ast_iterator.default_iterator.expr iter expr + in + let iterator = { Ast_iterator.default_iterator with expr = expr_iter } in + (match Mpipeline.reader_parsetree pipeline with + | `Implementation s -> iterator.structure iterator s + | `Interface _ -> ()); + None + with Found e -> Some e + +let find_parsetree_loc_exn pipeline loc = + Option.value_exn (find_parsetree_loc pipeline loc) + +(** [strip_attribute name e] removes all instances of the attribute called + [name] in [e]. *) +let strip_attribute attr_name expr = + let module M = Ocaml_parsing.Ast_mapper in + let expr_map (map : M.mapper) expr = + { (M.default_mapper.expr map expr) with + pexp_attributes = + List.filter expr.pexp_attributes ~f:(fun (a : Parsetree.attribute) -> + not (String.equal a.attr_name.txt attr_name)) + } + in + let mapper = { M.default_mapper with expr = expr_map } in + mapper.expr mapper expr + +(** Overapproximation of the number of uses of a [Path.t] in an expression. *) +module Uses : sig + type t + + val find : t -> Path.t -> int option + + val of_typedtree : Typedtree.expression -> t +end = struct + type t = int Path.Map.t + + let find m k = Path.Map.find_opt k m + + let of_typedtree (expr : Typedtree.expression) = + let module I = Ocaml_typing.Tast_iterator in + let uses = ref Path.Map.empty in + let expr_iter (iter : I.iterator) (expr : Typedtree.expression) = + match expr.exp_desc with + | Texp_ident (path, _, _) -> + uses := + Path.Map.update + path + (function + | Some c -> Some (c + 1) + | None -> Some 1) + !uses + | _ -> I.default_iterator.expr iter expr + in + let iterator = { I.default_iterator with expr = expr_iter } in + iterator.expr iterator expr; + !uses +end + +(** Mapping from [Location.t] to [Path.t]. Computed from the typedtree. Useful + for determining whether two parsetree identifiers refer to the same path. *) +module Paths : sig + type t + + val find : t -> Loc.t -> Path.t option + + val of_typedtree : Typedtree.expression -> t + + val same_path : t -> Loc.t -> Loc.t -> bool +end = struct + type t = Path.t Loc.Map.t + + let find = Loc.Map.find + + let of_typedtree (expr : Typedtree.expression) = + let module I = Ocaml_typing.Tast_iterator in + let paths = ref Loc.Map.empty in + let expr_iter (iter : I.iterator) (expr : Typedtree.expression) = + match expr.exp_desc with + | Texp_ident (path, { loc; _ }, _) -> paths := Loc.Map.set !paths loc path + | _ -> I.default_iterator.expr iter expr + in + let pat_iter (type k) (iter : I.iterator) + (pat : k Typedtree.general_pattern) = + match pat.pat_desc with + | Tpat_var (id, { loc; _ }) -> paths := Loc.Map.set !paths loc (Pident id) + | Tpat_alias (pat, id, { loc; _ }) -> + paths := Loc.Map.set !paths loc (Pident id); + I.default_iterator.pat iter pat + | _ -> I.default_iterator.pat iter pat + in + let iterator = + { I.default_iterator with expr = expr_iter; pat = pat_iter } + in + iterator.expr iterator expr; + !paths + + let same_path ps l l' = + match (find ps l, find ps l') with + | Some p, Some p' -> Path.same p p' + | _ -> false +end + +let subst same subst_expr subst_id body = + let module M = Ocaml_parsing.Ast_mapper in + let expr_map (map : M.mapper) (expr : Parsetree.expression) = + match expr.pexp_desc with + | Pexp_ident id when same subst_id id -> subst_expr + | _ -> M.default_mapper.expr map expr + in + let mapper = { M.default_mapper with expr = expr_map } in + mapper.expr mapper body + +(** Rough check for expressions that can be duplicated without duplicating any + side effects. *) +let rec is_pure (expr : Parsetree.expression) = + match expr.pexp_desc with + | Pexp_ident _ | Pexp_constant _ | Pexp_hole | Pexp_unreachable -> true + | Pexp_field (e, _) | Pexp_constraint (e, _) -> is_pure e + | _ -> false + +let rec find_map_remove ~f = function + | [] -> (None, []) + | x :: xs -> ( + match f x with + | Some x' -> (Some x', xs) + | None -> + let ret, xs' = find_map_remove ~f xs in + (ret, x :: xs')) + +let rec beta_reduce (uses : Uses.t) (paths : Paths.t) + (app : Parsetree.expression) = + let rec beta_reduce_arg (pat : Parsetree.pattern) body arg = + let default () = + H.Exp.let_ Nonrecursive [ H.Vb.mk pat arg ] (beta_reduce uses paths body) + in + match pat.ppat_desc with + | Ppat_any | Ppat_construct ({ txt = Lident "()"; _ }, _) -> + beta_reduce uses paths body + | Ppat_var param | Ppat_constraint ({ ppat_desc = Ppat_var param; _ }, _) + -> ( + let open Option.O in + let m_uses = + let* path = Paths.find paths param.loc in + Uses.find uses path + in + let same_path paths (id : _ H.with_loc) (id' : _ H.with_loc) = + Paths.same_path paths id.loc id'.loc + in + match m_uses with + | Some 0 -> beta_reduce uses paths body + | Some 1 -> + beta_reduce uses paths (subst (same_path paths) arg param body) + | Some _ | None -> + if is_pure arg then + beta_reduce uses paths (subst (same_path paths) arg param body) + else + (* if the parameter is used multiple times in the body, introduce a + let binding so that the parameter is evaluated only once *) + default ()) + | Ppat_tuple pats -> ( + match arg.pexp_desc with + | Pexp_tuple args -> + List.fold_left2 + ~f:(fun body pat arg -> beta_reduce_arg pat body arg) + ~init:body + pats + args + | _ -> default ()) + | _ -> default () + in + let apply func args = + if List.is_empty args then func else H.Exp.apply func args + in + match app.pexp_desc with + | Pexp_apply + ( { pexp_desc = Pexp_fun (Nolabel, None, pat, body); _ } + , (Nolabel, arg) :: args' ) -> beta_reduce_arg pat (apply body args') arg + | Pexp_apply + ({ pexp_desc = Pexp_fun ((Labelled l as lbl), None, pat, body); _ }, args) + -> ( + let m_matching_arg, args' = + find_map_remove args ~f:(function + | Asttypes.Labelled l', e when String.equal l l' -> Some e + | _ -> None) + in + match m_matching_arg with + | Some arg -> beta_reduce_arg pat (apply body args') arg + | None -> H.Exp.fun_ lbl None pat (beta_reduce uses paths (apply body args)) + ) + | _ -> app + +let inlined_text pipeline task = + let open Option.O in + let+ expr = find_parsetree_loc pipeline task.inlined_expr.exp_loc in + let expr = strip_attribute "merlin.loc" expr in + Format.asprintf "(%a)" Pprintast.expression expr + +(** [inline_edits pipeline task] returns a list of inlining edits and an + optional error value. An error will be generated if any of the potential + inlinings is not allowed due to shadowing. The successful edits will still + be returned *) +let inline_edits pipeline task = + let module I = Ocaml_typing.Tast_iterator in + let open Option.O in + let+ newText = inlined_text pipeline task in + let make_edit newText loc = + TextEdit.create ~newText ~range:(Range.of_loc loc) + in + let edits = Queue.create () in + let error = ref None in + + let insert_edit newText loc = Queue.push edits (make_edit newText loc) in + let not_shadowed env = + match check_shadowing task.inlined_expr env with + | Ok () -> true + | Error e -> + error := Some e; + false + in + + (* inlining into an argument context has some special cases *) + let arg_iter env (iter : I.iterator) (label : Asttypes.arg_label) + (m_arg_expr : Typedtree.expression option) = + match (label, m_arg_expr) with + (* handle the labeled argument shorthand `f ~x` when inlining `x` *) + | ( Labelled name + , Some { exp_desc = Texp_ident (Pident id, { loc; _ }, _); _ } ) + (* inlining is allowed for optional arguments that are being passed a Some + parameter, i.e. `x` may be inlined in `let x = 1 in (fun ?(x = 0) -> x) + ~x` *) + | ( Optional name + , Some + { exp_desc = + (* construct is part of desugaring, assumed to be Some *) + Texp_construct + ( _ + , _ + , [ { exp_desc = Texp_ident (Pident id, { loc; _ }, _); _ } ] ) + ; _ + } ) + when Ident.same task.inlined_var id && not_shadowed env -> + let newText = sprintf "%s:%s" name newText in + insert_edit newText loc + | Optional _, Some ({ exp_desc = Texp_construct _; _ } as arg_expr) -> + iter.expr iter arg_expr + (* inlining is _not_ allowed for optional arguments that are being passed an + optional parameter i.e. `x` may _not_ be inlined in `let x = Some 1 in + (fun ?(x = 0) -> x) ?x` *) + | Optional _, Some _ -> () + | _, _ -> Option.iter m_arg_expr ~f:(iter.expr iter) + in + + let uses = Uses.of_typedtree task.inlined_expr in + let paths = Paths.of_typedtree task.inlined_expr in + let inlined_pexpr = + find_parsetree_loc_exn pipeline task.inlined_expr.exp_loc + in + + let expr_iter (iter : I.iterator) (expr : Typedtree.expression) = + match expr.exp_desc with + (* when inlining into an application context, attempt to beta reduce the + result *) + | Texp_apply ({ exp_desc = Texp_ident (Pident id, _, _); _ }, _) + when Ident.same task.inlined_var id && not_shadowed expr.exp_env -> + let reduced_pexpr = + let app_pexpr = find_parsetree_loc_exn pipeline expr.exp_loc in + match app_pexpr.pexp_desc with + | Pexp_apply ({ pexp_desc = Pexp_ident _; _ }, args) -> + beta_reduce uses paths (H.Exp.apply inlined_pexpr args) + | _ -> app_pexpr + in + let newText = + Format.asprintf "(%a)" Pprintast.expression + @@ strip_attribute "merlin.loc" reduced_pexpr + in + insert_edit newText expr.exp_loc + | Texp_apply (func, args) -> + iter.expr iter func; + List.iter args ~f:(fun (l, e) -> arg_iter expr.exp_env iter l e) + | Texp_ident (Pident id, { loc; _ }, _) + when Ident.same task.inlined_var id && not_shadowed expr.exp_env -> + insert_edit newText loc + | _ -> I.default_iterator.expr iter expr + in + let iterator = { I.default_iterator with expr = expr_iter } in + + let edits = + match Mtyper.get_typedtree (Mpipeline.typer_result pipeline) with + | `Interface _ -> [] + | `Implementation structure -> + iterator.structure iterator structure; + Queue.to_list edits + in + (edits, !error) + +let code_action doc (params : CodeActionParams.t) = + let open Option.O in + Document.with_pipeline_exn doc (fun pipeline -> + let* typedtree = + match Mtyper.get_typedtree (Mpipeline.typer_result pipeline) with + | `Interface _ -> None + | `Implementation x -> Some x + in + let* task = find_inline_task typedtree params.range.start in + inline_edits pipeline task) + |> Fiber.map ~f:(fun m_edits -> + let* edits, m_error = m_edits in + match (edits, m_error) with + | [], None -> None + | [], Some error -> + let action = + CodeAction.create + ~title:action_title + ~kind:CodeActionKind.RefactorInline + ~isPreferred:false + ~disabled: + (CodeAction.create_disabled ~reason:(string_of_error error)) + () + in + Some action + | _ :: _, (Some _ | None) -> + let edit = + let version = Document.version doc in + let textDocument = + OptionalVersionedTextDocumentIdentifier.create + ~uri:params.textDocument.uri + ~version + () + in + let edit = + TextDocumentEdit.create + ~textDocument + ~edits:(List.map edits ~f:(fun e -> `TextEdit e)) + in + WorkspaceEdit.create ~documentChanges:[ `TextDocumentEdit edit ] () + in + let action = + CodeAction.create + ~title:action_title + ~kind:CodeActionKind.RefactorInline + ~edit + ~isPreferred:false + () + in + Some action) + +let t = { Code_action.kind = RefactorInline; run = code_action } diff --git a/ocaml-lsp-server/src/code_actions/action_inline.mli b/ocaml-lsp-server/src/code_actions/action_inline.mli new file mode 100644 index 000000000..0caac27b3 --- /dev/null +++ b/ocaml-lsp-server/src/code_actions/action_inline.mli @@ -0,0 +1 @@ +val t : Code_action.t diff --git a/ocaml-lsp-server/src/import.ml b/ocaml-lsp-server/src/import.ml index dc3df96e2..80eb584ba 100644 --- a/ocaml-lsp-server/src/import.ml +++ b/ocaml-lsp-server/src/import.ml @@ -110,15 +110,42 @@ module Ast_iterator = Ocaml_parsing.Ast_iterator module Asttypes = Ocaml_parsing.Asttypes module Cmt_format = Ocaml_typing.Cmt_format module Ident = Ocaml_typing.Ident +module Env = Ocaml_typing.Env module Loc = struct - include Ocaml_parsing.Location - include Ocaml_parsing.Location_aux + module T = struct + include Ocaml_parsing.Location + include Ocaml_parsing.Location_aux + end + + include T + + module Map = Map.Make (struct + include T + + let compare x x' = Ordering.of_int (compare x x') + + let position_to_dyn (pos : Lexing.position) = + Dyn.Record + [ ("pos_fname", Dyn.String pos.pos_fname) + ; ("pos_lnum", Dyn.Int pos.pos_lnum) + ; ("pos_bol", Dyn.Int pos.pos_bol) + ; ("pos_cnum", Dyn.Int pos.pos_cnum) + ] + + let to_dyn loc = + Dyn.Record + [ ("loc_start", position_to_dyn loc.loc_start) + ; ("loc_end", position_to_dyn loc.loc_end) + ; ("loc_ghost", Dyn.Bool loc.loc_ghost) + ] + end) end module Longident = Ocaml_parsing.Longident module Parsetree = Ocaml_parsing.Parsetree module Path = Ocaml_typing.Path +module Pprintast = Ocaml_parsing.Pprintast module Typedtree = Ocaml_typing.Typedtree module Types = Ocaml_typing.Types module Warnings = Ocaml_utils.Warnings diff --git a/ocaml-lsp-server/src/ocaml_lsp_server.ml b/ocaml-lsp-server/src/ocaml_lsp_server.ml index e1ffcf752..21168fff8 100644 --- a/ocaml-lsp-server/src/ocaml_lsp_server.ml +++ b/ocaml-lsp-server/src/ocaml_lsp_server.ml @@ -38,6 +38,7 @@ let initialize_info (client_capabilities : ClientCapabilities.t) : ; Action_refactor_open.unqualify ; Action_refactor_open.qualify ; Action_add_rec.t + ; Action_inline.t ] |> List.sort_uniq ~compare:Poly.compare in diff --git a/ocaml-lsp-server/test/e2e-new/action_inline.ml b/ocaml-lsp-server/test/e2e-new/action_inline.ml new file mode 100644 index 000000000..daff1816c --- /dev/null +++ b/ocaml-lsp-server/test/e2e-new/action_inline.ml @@ -0,0 +1,518 @@ +open Test.Import +open Code_actions + +let parse_cursor src = + let cursor = + String.split_lines src + |> List.find_mapi ~f:(fun lnum line -> + match String.index line '$' with + | Some cnum -> Some (Position.create ~character:cnum ~line:lnum) + | None -> None) + |> Option.value_exn + in + ( String.filter_map src ~f:(function + | '$' -> None + | c -> Some c) + , Range.create ~start:cursor ~end_:cursor ) + +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_edit (edit : TextEdit.t) src = + let start_offset = offset_of_position src edit.range.start in + let end_offset = offset_of_position src edit.range.end_ in + let start = String.take src start_offset in + let end_ = String.drop src end_offset in + start ^ edit.newText ^ end_ + +let apply_inline_action src range = + let open Option.O in + let code_actions = ref None in + iter_code_actions src range (fun ca -> code_actions := Some ca); + let* m_code_actions = !code_actions in + let* code_actions = m_code_actions in + let* { documentChanges; _ } = + List.find_map code_actions ~f:(function + | `CodeAction { kind = Some RefactorInline; edit = Some edit; _ } -> + Some edit + | _ -> None) + in + let+ documentChanges in + let edits = + List.filter_map documentChanges ~f:(function + | `TextDocumentEdit e -> Some e + | _ -> None) + in + match edits with + | [] -> src + | [ { edits = [ `TextEdit e ]; _ } ] -> apply_edit e src + | _ -> failwith "expected one edit" + +let inline_test src = + let src, range = parse_cursor src in + Option.iter (apply_inline_action src range) ~f:print_string + +let%expect_test "" = + inline_test {| +let _ = + let $x = 0 in + x + 1 +|}; + [%expect {| + let _ = + let x = 0 in + (0) + 1 |}] + +let%expect_test "shadow-1" = + inline_test + {| +let _ = + let y = 1 in + let $x = y in + let y = 0 in + x + 1 +|}; + [%expect {| |}] + +let%expect_test "shadow-2" = + inline_test + {| +let _ = + let y = 1 in + let $x y = y in + let y = 0 in + x y + 1 +|}; + [%expect + {| + let _ = + let y = 1 in + let x y = y in + let y = 0 in + (y) + 1 |}] + +let%expect_test "shadow-3" = + inline_test + {| +let _ = + let y = 1 in + let $x z = y + z in + let y = 0 in + x y + 1 +|}; + [%expect {| |}] + +let%expect_test "shadow-4" = + inline_test + {| +module M = struct + let y = 1 +end +let _ = + let $x = M.y in + let module M = struct + let y = 2 + end in + x +|}; + [%expect {| |}] + +let%expect_test "shadow-5" = + inline_test + {| +module M = struct + let y = 1 +end +let _ = + let $x = M.y in + let module N = struct + let y = 2 + end in + x +|}; + [%expect + {| + module M = struct + let y = 1 + end + let _ = + let x = M.y in + let module N = struct + let y = 2 + end in + (M.y) |}] + +let%expect_test "" = + inline_test {| +let _ = + let $x = 0 + 1 in + (fun x -> x) x +|}; + [%expect {| + let _ = + let x = 0 + 1 in + (fun x -> x) (0 + 1) |}] + +let%expect_test "" = + inline_test {| +let _ = + let $x = 0 + 1 in + (fun ~x -> x) ~x +|}; + [%expect + {| + let _ = + let x = 0 + 1 in + (fun ~x -> x) ~x:(0 + 1) |}] + +let%expect_test "" = + inline_test {| +let _ = + let $x = 0 + 1 in + (fun ?(x = 2) -> x) ~x +|}; + [%expect + {| + let _ = + let x = 0 + 1 in + (fun ?(x = 2) -> x) ~x:(0 + 1) |}] + +let%expect_test "" = + inline_test {| +let _ = + let $x = Some 0 in + (fun ?(x = 2) -> x) ?x +|}; + [%expect {| |}] + +let%expect_test "" = + inline_test {| +let _ = + let $x = 0 in + (fun ~x -> x) ~x:(x + 1) +|}; + [%expect + {| + let _ = + let x = 0 in + (fun ~x -> x) ~x:((0) + 1) |}] + +let%expect_test "" = + inline_test {| +let _ = + let $x = 0 in + (fun ?(x = 1) -> x) ~x:(x + 1) +|}; + [%expect + {| + let _ = + let x = 0 in + (fun ?(x = 1) -> x) ~x:((0) + 1) |}] + +let%expect_test "" = + inline_test {| +let _ = + let $f x = x in + f 1 +|}; + [%expect {| + let _ = + let f x = x in + (1) |}] + +let%expect_test "" = + inline_test {| +let _ = + let $f _ = 0 in + f 1 +|}; + [%expect {| + let _ = + let f _ = 0 in + (0) |}] + +let%expect_test "" = + inline_test {| +let _ = + let $f x = x + x in + f 1 +|}; + [%expect {| + let _ = + let f x = x + x in + (1 + 1) |}] + +let%expect_test "" = + inline_test {| +let _ = + let $f x = x + x in + f (g 1) +|}; + [%expect + {| + let _ = + let f x = x + x in + (let x = g 1 in x + x) |}] + +let%expect_test "" = + inline_test {| +let _ = + let $f x y = x + y in + f 0 +|}; + [%expect {| + let _ = + let f x y = x + y in + (fun y -> 0 + y) |}] + +let%expect_test "" = + inline_test {| +let _ = + let $f x ~y = x + y in + f ~y:0 +|}; + [%expect + {| + let _ = + let f x ~y = x + y in + ((fun x ~y -> x + y) ~y:0) |}] + +let%expect_test "" = + inline_test {| +let _ = + let $f ~x y = x + y in + f ~x:0 +|}; + [%expect {| + let _ = + let f ~x y = x + y in + (fun y -> 0 + y) |}] + +let%expect_test "" = + inline_test {| +let _ = + let $f ~x ~y = x + y in + f ~y:0 +|}; + [%expect + {| + let _ = + let f ~x ~y = x + y in + (fun ~x -> x + 0) |}] + +let%expect_test "" = + inline_test {| +let _ = + let $f (x : int) = x + 1 in + f 0 +|}; + [%expect {| + let _ = + let f (x : int) = x + 1 in + (0 + 1) |}] + +(* TODO: allow beta reduction with locally abstract types *) +let%expect_test "" = + inline_test {| +let _ = + let $f (type a) (x : a) = x in + f 0 +|}; + [%expect + {| + let _ = + let f (type a) (x : a) = x in + ((fun (type a) -> fun (x : a) -> x) 0) |}] + +let%expect_test "" = + inline_test {| +let _ = + let $f : int -> int = fun x -> x in + f 0 +|}; + [%expect {| + let _ = + let f : int -> int = fun x -> x in + (0) |}] + +let%expect_test "" = + inline_test + {| +let _ = + let $f = function Some x -> x | None -> 0 in + f (Some 1) +|}; + [%expect + {| + let _ = + let f = function Some x -> x | None -> 0 in + ((function | Some x -> x | None -> 0) (Some 1)) |}] + +(* TODO: allow beta reduction with `as` *) +let%expect_test "" = + inline_test {| +let _ = + let $f (x as y) = y + 1 in + f 1 +|}; + [%expect + {| + let _ = + let f (x as y) = y + 1 in + (let x as y = 1 in y + 1) |}] + +let%expect_test "" = + inline_test {| +let _ = + let $f 1 = 2 in + f 2 +|}; + [%expect {| + let _ = + let f 1 = 2 in + (let 1 = 2 in 2) |}] + +let%expect_test "" = + inline_test {| +let _ = + let $f (x, y) = x + y in + f (1, 2) +|}; + [%expect {| + let _ = + let f (x, y) = x + y in + (1 + 2) |}] + +let%expect_test "" = + inline_test {| +let _ = + let $f (x, y) = x + y + y in + f (1, 2 + 3) +|}; + [%expect + {| + let _ = + let f (x, y) = x + y + y in + (let y = 2 + 3 in (1 + y) + y) |}] + +let%expect_test "" = + inline_test + {| +let _ = + let $f (x, y) = x + y + y in + let z = (1, 2) in + f z +|}; + [%expect + {| + let _ = + let f (x, y) = x + y + y in + let z = (1, 2) in + (let (x, y) = z in (x + y) + y) |}] + +(* TODO *) +let%expect_test "" = + inline_test + {| +type t = { x : int; y : int } +let _ = + let $f { x; y } = x + y in + f { x = 1; y = 1 } +|}; + [%expect + {| + type t = { x : int; y : int } + let _ = + let f { x; y } = x + y in + (let { x; y } = { x = 1; y = 1 } in x + y) |}] + +(* TODO: beta reduce record literals as with tuples *) +let%expect_test "" = + inline_test + {| +type t = { x : int; y : int } +let _ = + let $f { x; _ } = x + 1 in + f { x = 1; y = 1 } +|}; + [%expect + {| + type t = { x : int; y : int } + let _ = + let f { x; _ } = x + 1 in + (let { x;_} = { x = 1; y = 1 } in x + 1) |}] + +let%expect_test "" = + inline_test {| +let _ = + let $f x = [%test] x in + f 1 +|}; + [%expect {| + let _ = + let f x = [%test] x in + (([%test ]) 1) |}] + +let%expect_test "" = + inline_test {| +let _ = + let $f x = x in + [%test] (f 1) +|}; + [%expect {| + let _ = + let f x = x in + [%test] (1) |}] + +let%expect_test "" = + inline_test {| +let _ = + let $f x = (* test comment *) x in + f 1 +|}; + [%expect {| + let _ = + let f x = (* test comment *) x in + (1) |}] + +let%expect_test "" = + inline_test {| +let _ = + let $f x = x in + (* test comment *) f 1 +|}; + [%expect {| + let _ = + let f x = x in + (* test comment *) (1) |}] + +let%expect_test "" = + inline_test {| +let $f x = x +let g y = f y +|}; + [%expect {| + let f x = x + let g y = (y) |}] + +(* TODO *) +let%expect_test "" = + inline_test + {| +module M = struct + let $f x = x + let g y = f y +end +let h = M.f +|}; + [%expect + {| + module M = struct + let f x = x + let g y = (y) + end + let h = M.f |}] diff --git a/ocaml-lsp-server/test/e2e-new/code_actions.ml b/ocaml-lsp-server/test/e2e-new/code_actions.ml index 68b65c44c..292973ddd 100644 --- a/ocaml-lsp-server/test/e2e-new/code_actions.ml +++ b/ocaml-lsp-server/test/e2e-new/code_actions.ml @@ -1,54 +1,47 @@ open Test.Import -let%expect_test "code actions" = - let source = {ocaml| -let foo = 123 -|ocaml} in +let iter_code_actions ?(path = "foo.ml") source range k = let handler = Client.Handler.make ~on_notification:(fun _ _ -> Fiber.return ()) () in - ( Test.run ~handler @@ fun client -> - let run_client () = - let capabilities = - let window = - let showDocument = - ShowDocumentClientCapabilities.create ~support:true - in - WindowClientCapabilities.create ~showDocument () + Test.run ~handler @@ fun client -> + let run_client () = + let capabilities = + let window = + let showDocument = + ShowDocumentClientCapabilities.create ~support:true in - ClientCapabilities.create ~window () + WindowClientCapabilities.create ~showDocument () in - Client.start client (InitializeParams.create ~capabilities ()) + ClientCapabilities.create ~window () in - let run = - let* (_ : InitializeResult.t) = Client.initialized client in - let uri = DocumentUri.of_path "foo.ml" in - let* () = - let textDocument = - TextDocumentItem.create - ~uri - ~languageId:"ocaml" - ~version:0 - ~text:source - in - Client.notification - client - (TextDocumentDidOpen (DidOpenTextDocumentParams.create ~textDocument)) + Client.start client (InitializeParams.create ~capabilities ()) + in + let run = + let* (_ : InitializeResult.t) = Client.initialized client in + let uri = DocumentUri.of_path path in + let* () = + let textDocument = + TextDocumentItem.create ~uri ~languageId:"ocaml" ~version:0 ~text:source in - let+ resp = - let range = - let start = Position.create ~line:1 ~character:5 in - let end_ = Position.create ~line:1 ~character:7 in - Range.create ~start ~end_ - in - let context = CodeActionContext.create ~diagnostics:[] () in - let request = - let textDocument = TextDocumentIdentifier.create ~uri in - CodeActionParams.create ~textDocument ~range ~context () - in - Client.request client (CodeAction request) + Client.notification + client + (TextDocumentDidOpen (DidOpenTextDocumentParams.create ~textDocument)) + in + let+ resp = + let context = CodeActionContext.create ~diagnostics:[] () in + let request = + let textDocument = TextDocumentIdentifier.create ~uri in + CodeActionParams.create ~textDocument ~range ~context () in - match resp with + Client.request client (CodeAction request) + in + k resp + in + Fiber.fork_and_join_unit run_client (fun () -> run >>> Client.stop client) + +let print_code_actions ?(path = "foo.ml") source range = + iter_code_actions ~path source range (function | None -> print_endline "no code actions" | Some code_actions -> print_endline "Code actions:"; @@ -58,10 +51,18 @@ let foo = 123 | `Command command -> Command.yojson_of_t command | `CodeAction ca -> CodeAction.yojson_of_t ca in - Yojson.Safe.pretty_to_string ~std:false json |> print_endline) - in - Fiber.fork_and_join_unit run_client (fun () -> run >>> Client.stop client) - ); + Yojson.Safe.pretty_to_string ~std:false json |> print_endline)) + +let%expect_test "code actions" = + let source = {ocaml| +let foo = 123 +|ocaml} in + let range = + let start = Position.create ~line:1 ~character:5 in + let end_ = Position.create ~line:1 ~character:7 in + Range.create ~start ~end_ + in + print_code_actions source range; [%expect {| Code actions: diff --git a/ocaml-lsp-server/test/e2e-new/start_stop.ml b/ocaml-lsp-server/test/e2e-new/start_stop.ml index 1997f3705..34f724f23 100644 --- a/ocaml-lsp-server/test/e2e-new/start_stop.ml +++ b/ocaml-lsp-server/test/e2e-new/start_stop.ml @@ -35,8 +35,8 @@ let%expect_test "start/stop" = "capabilities": { "codeActionProvider": { "codeActionKinds": [ - "quickfix", "construct", "destruct", "inferred_intf", - "put module name in identifiers", + "quickfix", "refactor.inline", "construct", "destruct", + "inferred_intf", "put module name in identifiers", "remove module name from identifiers", "type-annotate" ] }, diff --git a/ocaml-lsp-server/test/e2e-new/test.ml b/ocaml-lsp-server/test/e2e-new/test.ml index fba7ed7e9..45be35b55 100644 --- a/ocaml-lsp-server/test/e2e-new/test.ml +++ b/ocaml-lsp-server/test/e2e-new/test.ml @@ -1,5 +1,32 @@ module Import = struct - include Stdune + 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 + end + include Fiber.O module Client = Lsp_fiber.Client include Lsp.Types