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

1712 lines
42 KiB
OCaml

open Test.Import
open Lsp_helpers
let iter_code_actions ?prep ?path ?(diagnostics = []) ~source range =
let makeRequest textDocument =
let context = CodeActionContext.create ~diagnostics () in
Lsp.Client_request.CodeAction
(CodeActionParams.create ~textDocument ~range ~context ())
in
iter_lsp_response ?prep ?path ~makeRequest ~source
;;
let print_code_actions
?(prep = fun _ -> Fiber.return ())
?(path = "foo.ml")
?(filter = fun _ -> true)
source
range
=
iter_code_actions ~prep ~path ~source range (function
| None -> print_endline "No code actions"
| Some code_actions ->
code_actions
|> List.filter ~f:filter
|> (function
| [] -> print_endline "No code actions"
| actions ->
print_endline "Code actions:";
List.iter actions ~f:(fun ca ->
let json =
match ca with
| `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)))
;;
let find_action action_name action =
match action with
| `CodeAction { CodeAction.kind = Some (Other name); _ } -> name = action_name
| _ -> false
;;
let find_annotate_action = find_action "type-annotate"
let find_remove_annotation_action = find_action "remove type annotation"
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:
{
"edit": {
"documentChanges": [
{
"edits": [
{
"newText": "(foo : int)",
"range": {
"end": { "character": 7, "line": 1 },
"start": { "character": 4, "line": 1 }
}
}
],
"textDocument": { "uri": "file:///foo.ml", "version": 0 }
}
]
},
"isPreferred": false,
"kind": "type-annotate",
"title": "Type-annotate"
}
{
"command": {
"arguments": [ "file:///foo.mli" ],
"command": "ocamllsp/open-related-source",
"title": "Create foo.mli"
},
"edit": {
"documentChanges": [ { "kind": "create", "uri": "file:///foo.mli" } ]
},
"kind": "switch",
"title": "Create foo.mli"
} |}]
;;
let%expect_test "can type-annotate a function argument" =
let source =
{ocaml|
type t = Foo of int | Bar of bool
let f x = Foo x
|ocaml}
in
let range =
let start = Position.create ~line:2 ~character:6 in
let end_ = Position.create ~line:2 ~character:7 in
Range.create ~start ~end_
in
print_code_actions source range ~filter:find_annotate_action;
[%expect
{|
Code actions:
{
"edit": {
"documentChanges": [
{
"edits": [
{
"newText": "(x : int)",
"range": {
"end": { "character": 7, "line": 2 },
"start": { "character": 6, "line": 2 }
}
}
],
"textDocument": { "uri": "file:///foo.ml", "version": 0 }
}
]
},
"isPreferred": false,
"kind": "type-annotate",
"title": "Type-annotate"
} |}]
;;
let%expect_test "can type-annotate a toplevel value" =
let source =
{ocaml|
let iiii = 3 + 4
|ocaml}
in
let range =
let start = Position.create ~line:1 ~character:4 in
let end_ = Position.create ~line:1 ~character:5 in
Range.create ~start ~end_
in
print_code_actions source range;
[%expect
{|
Code actions:
{
"edit": {
"documentChanges": [
{
"edits": [
{
"newText": "(iiii : int)",
"range": {
"end": { "character": 8, "line": 1 },
"start": { "character": 4, "line": 1 }
}
}
],
"textDocument": { "uri": "file:///foo.ml", "version": 0 }
}
]
},
"isPreferred": false,
"kind": "type-annotate",
"title": "Type-annotate"
}
{
"command": {
"arguments": [ "file:///foo.mli" ],
"command": "ocamllsp/open-related-source",
"title": "Create foo.mli"
},
"edit": {
"documentChanges": [ { "kind": "create", "uri": "file:///foo.mli" } ]
},
"kind": "switch",
"title": "Create foo.mli"
}
|}]
;;
let%expect_test "does not type-annotate function" =
let source =
{ocaml|
let my_fun x y = 1
|ocaml}
in
let range =
let start = Position.create ~line:1 ~character:5 in
let end_ = Position.create ~line:1 ~character:6 in
Range.create ~start ~end_
in
print_code_actions source range ~filter:find_annotate_action;
[%expect {| No code actions |}]
;;
let%expect_test "can type-annotate an argument in a function call" =
let source =
{ocaml|
let f x = x + 1
let () =
let i = 8 in
print_int (f i)
|ocaml}
in
let range =
let start = Position.create ~line:1 ~character:7 in
let end_ = Position.create ~line:1 ~character:8 in
Range.create ~start ~end_
in
print_code_actions source range ~filter:find_annotate_action;
[%expect
{|
Code actions:
{
"edit": {
"documentChanges": [
{
"edits": [
{
"newText": "(x : int)",
"range": {
"end": { "character": 7, "line": 1 },
"start": { "character": 6, "line": 1 }
}
}
],
"textDocument": { "uri": "file:///foo.ml", "version": 0 }
}
]
},
"isPreferred": false,
"kind": "type-annotate",
"title": "Type-annotate"
} |}]
;;
let%expect_test "can type-annotate a variant with its name only" =
let source =
{ocaml|
type t = Foo of int | Bar of bool
let f (x : t) = x
|ocaml}
in
let range =
let start = Position.create ~line:3 ~character:16 in
let end_ = Position.create ~line:3 ~character:17 in
Range.create ~start ~end_
in
print_code_actions source range ~filter:find_annotate_action;
[%expect
{|
Code actions:
{
"edit": {
"documentChanges": [
{
"edits": [
{
"newText": "(x : t)",
"range": {
"end": { "character": 17, "line": 3 },
"start": { "character": 16, "line": 3 }
}
}
],
"textDocument": { "uri": "file:///foo.ml", "version": 0 }
}
]
},
"isPreferred": false,
"kind": "type-annotate",
"title": "Type-annotate"
} |}]
;;
let%expect_test "does not type-annotate in a non expression context" =
let source =
{ocaml|
type x =
| Foo of int
| Baz of string
|ocaml}
in
let range =
let start = Position.create ~line:3 ~character:5 in
let end_ = Position.create ~line:3 ~character:6 in
Range.create ~start ~end_
in
print_code_actions source range ~filter:find_annotate_action;
[%expect {| No code actions |}]
;;
let%expect_test "does not type-annotate already annotated argument" =
let source =
{ocaml|
let f (x : int) = 1
|ocaml}
in
let range =
let start = Position.create ~line:1 ~character:7 in
let end_ = Position.create ~line:1 ~character:8 in
Range.create ~start ~end_
in
print_code_actions source range ~filter:find_annotate_action;
[%expect {| No code actions |}]
;;
let%expect_test "does not type-annotate already annotated expression" =
let source =
{ocaml|
let f x = (1 : int)
|ocaml}
in
let range =
let start = Position.create ~line:1 ~character:11 in
let end_ = Position.create ~line:1 ~character:12 in
Range.create ~start ~end_
in
print_code_actions source range ~filter:find_annotate_action;
[%expect {| No code actions |}]
;;
let%expect_test "does not type-annotate already annotated and coerced expression" =
let source =
{ocaml|
let f x = (1 : int :> int)
|ocaml}
in
let range =
let start = Position.create ~line:1 ~character:11 in
let end_ = Position.create ~line:1 ~character:12 in
Range.create ~start ~end_
in
print_code_actions source range ~filter:find_annotate_action;
[%expect {| No code actions |}]
;;
let%expect_test "can remove type annotation from a function argument" =
let source =
{ocaml|
type t = Foo of int | Bar of bool
let f (x : t) = Foo x
|ocaml}
in
let range =
let start = Position.create ~line:2 ~character:7 in
let end_ = Position.create ~line:2 ~character:8 in
Range.create ~start ~end_
in
print_code_actions source range ~filter:find_remove_annotation_action;
[%expect
{|
Code actions:
{
"edit": {
"documentChanges": [
{
"edits": [
{
"newText": "x",
"range": {
"end": { "character": 13, "line": 2 },
"start": { "character": 6, "line": 2 }
}
}
],
"textDocument": { "uri": "file:///foo.ml", "version": 0 }
}
]
},
"isPreferred": false,
"kind": "remove type annotation",
"title": "Remove type annotation"
} |}]
;;
let%expect_test "can remove type annotation from a toplevel value" =
let source =
{ocaml|
let (iiii : int) = 3 + 4
|ocaml}
in
let range =
let start = Position.create ~line:1 ~character:5 in
let end_ = Position.create ~line:1 ~character:6 in
Range.create ~start ~end_
in
print_code_actions source range ~filter:find_remove_annotation_action;
[%expect
{|
Code actions:
{
"edit": {
"documentChanges": [
{
"edits": [
{
"newText": "iiii",
"range": {
"end": { "character": 16, "line": 1 },
"start": { "character": 4, "line": 1 }
}
}
],
"textDocument": { "uri": "file:///foo.ml", "version": 0 }
}
]
},
"isPreferred": false,
"kind": "remove type annotation",
"title": "Remove type annotation"
} |}]
;;
let%expect_test "can remove type annotation from an argument in a function call" =
let source =
{ocaml|
let f (x : int) = x + 1
let () =
let i = 8 in
print_int (f i)
|ocaml}
in
let range =
let start = Position.create ~line:1 ~character:7 in
let end_ = Position.create ~line:1 ~character:8 in
Range.create ~start ~end_
in
print_code_actions source range ~filter:find_remove_annotation_action;
[%expect
{|
Code actions:
{
"edit": {
"documentChanges": [
{
"edits": [
{
"newText": "x",
"range": {
"end": { "character": 15, "line": 1 },
"start": { "character": 6, "line": 1 }
}
}
],
"textDocument": { "uri": "file:///foo.ml", "version": 0 }
}
]
},
"isPreferred": false,
"kind": "remove type annotation",
"title": "Remove type annotation"
} |}]
;;
let%expect_test "can remove type annotation from a coerced expression" =
let source =
{ocaml|
let x = (7 : int :> int)
|ocaml}
in
let range =
let start = Position.create ~line:1 ~character:9 in
let end_ = Position.create ~line:1 ~character:10 in
Range.create ~start ~end_
in
print_code_actions source range ~filter:find_remove_annotation_action;
[%expect
{|
Code actions:
{
"edit": {
"documentChanges": [
{
"edits": [
{
"newText": "7",
"range": {
"end": { "character": 16, "line": 1 },
"start": { "character": 9, "line": 1 }
}
}
],
"textDocument": { "uri": "file:///foo.ml", "version": 0 }
}
]
},
"isPreferred": false,
"kind": "remove type annotation",
"title": "Remove type annotation"
} |}]
;;
let%expect_test "does not remove type annotation from function" =
let source =
{ocaml|
let my_fun x y : int = 1
|ocaml}
in
let range =
let start = Position.create ~line:1 ~character:5 in
let end_ = Position.create ~line:1 ~character:6 in
Range.create ~start ~end_
in
print_code_actions source range ~filter:find_remove_annotation_action;
[%expect {| No code actions |}]
;;
let%expect_test "can destruct sum types" =
let source =
{ocaml|
type t = Foo of int | Bar of bool
let f (x : t) = x
|ocaml}
in
let range =
let start = Position.create ~line:2 ~character:16 in
let end_ = Position.create ~line:2 ~character:17 in
Range.create ~start ~end_
in
print_code_actions source range ~filter:(find_action "destruct (enumerate cases)");
[%expect
{|
Code actions:
{
"edit": {
"documentChanges": [
{
"edits": [
{
"newText": "match x with Foo _ -> _ | Bar _ -> _\n",
"range": {
"end": { "character": 17, "line": 2 },
"start": { "character": 16, "line": 2 }
}
}
],
"textDocument": { "uri": "file:///foo.ml", "version": 0 }
}
]
},
"isPreferred": false,
"kind": "destruct (enumerate cases)",
"title": "Destruct (enumerate cases)"
}
|}]
;;
let%expect_test "can destruct match line" =
let source =
{ocaml|
let f (x:bool) =
match x
|ocaml}
in
let range =
let start = Position.create ~line:2 ~character:5 in
let end_ = Position.create ~line:2 ~character:5 in
Range.create ~start ~end_
in
print_code_actions
source
range
~filter:(find_action "destruct-line (enumerate cases, use existing match)");
[%expect
{|
Code actions:
{
"edit": {
"documentChanges": [
{
"edits": [
{
"newText": "match x with\n | false -> _\n | true -> _",
"range": {
"end": { "character": 9, "line": 2 },
"start": { "character": 2, "line": 2 }
}
}
],
"textDocument": { "uri": "file:///foo.ml", "version": 0 }
}
]
},
"isPreferred": false,
"kind": "destruct-line (enumerate cases, use existing match)",
"title": "Destruct-line (enumerate cases, use existing match)"
}
|}]
;;
let%expect_test "can destruct match-with line" =
let source =
{ocaml|
match (Ok 0) with
|ocaml}
in
let range =
let start = Position.create ~line:1 ~character:0 in
let end_ = Position.create ~line:1 ~character:0 in
Range.create ~start ~end_
in
print_code_actions
source
range
~filter:(find_action "destruct-line (enumerate cases, use existing match)");
[%expect
{|
Code actions:
{
"edit": {
"documentChanges": [
{
"edits": [
{
"newText": "match Ok 0 with\n | Ok _ -> _\n | Error _ -> _",
"range": {
"end": { "character": 21, "line": 1 },
"start": { "character": 4, "line": 1 }
}
}
],
"textDocument": { "uri": "file:///foo.ml", "version": 0 }
}
]
},
"isPreferred": false,
"kind": "destruct-line (enumerate cases, use existing match)",
"title": "Destruct-line (enumerate cases, use existing match)"
}
|}]
;;
let%expect_test "can destruct case line" =
let source =
{ocaml|
type q =
| A
| B
| C
| D
let f (x: q) =
match x with
| C -> _
|ocaml}
in
let range =
let start = Position.create ~line:8 ~character:0 in
let end_ = Position.create ~line:8 ~character:0 in
Range.create ~start ~end_
in
print_code_actions
source
range
~filter:(find_action "destruct-line (enumerate cases, use existing match)");
[%expect
{|
Code actions:
{
"edit": {
"documentChanges": [
{
"edits": [
{
"newText": "\n | A -> _\n | B -> _\n | D -> _",
"range": {
"end": { "character": 10, "line": 8 },
"start": { "character": 10, "line": 8 }
}
}
],
"textDocument": { "uri": "file:///foo.ml", "version": 0 }
}
]
},
"isPreferred": false,
"kind": "destruct-line (enumerate cases, use existing match)",
"title": "Destruct-line (enumerate cases, use existing match)"
}
|}]
;;
let%expect_test "can destruct hole" =
let source =
{ocaml|
let zip (type a b) (xs : a list) (ys : b list) : (a * b) list =
match (xs, ys) with
| (_, _) -> _
|ocaml}
in
let range =
let start = Position.create ~line:3 ~character:5 in
let end_ = Position.create ~line:3 ~character:5 in
Range.create ~start ~end_
in
print_code_actions
source
range
~filter:(find_action "destruct-line (enumerate cases, use existing match)");
[%expect
{|
Code actions:
{
"edit": {
"documentChanges": [
{
"edits": [
{
"newText": "([], _) -> _\n | (_::_, _)",
"range": {
"end": { "character": 10, "line": 3 },
"start": { "character": 4, "line": 3 }
}
}
],
"textDocument": { "uri": "file:///foo.ml", "version": 0 }
}
]
},
"isPreferred": false,
"kind": "destruct-line (enumerate cases, use existing match)",
"title": "Destruct-line (enumerate cases, use existing match)"
}
|}]
;;
let%expect_test "destruct hole spacing" =
let source =
{ocaml|
type q =
| A
| B
| C
| D
let f (x: q) =
match x with
| _ -> _
|ocaml}
in
let range =
let start = Position.create ~line:8 ~character:5 in
let end_ = Position.create ~line:8 ~character:5 in
Range.create ~start ~end_
in
print_code_actions
source
range
~filter:(find_action "destruct-line (enumerate cases, use existing match)");
[%expect
{|
Code actions:
{
"edit": {
"documentChanges": [
{
"edits": [
{
"newText": "A -> _\n | B -> _\n | C -> _\n | D",
"range": {
"end": { "character": 5, "line": 8 },
"start": { "character": 4, "line": 8 }
}
}
],
"textDocument": { "uri": "file:///foo.ml", "version": 0 }
}
]
},
"isPreferred": false,
"kind": "destruct-line (enumerate cases, use existing match)",
"title": "Destruct-line (enumerate cases, use existing match)"
}
|}]
;;
let%expect_test "destruct a case with a hole but not on the hole" =
let source =
{ocaml|
type q =
| A
| B
| C
| D
let f (x: q) =
match x with
| _ -> _
|ocaml}
in
let range =
let start = Position.create ~line:8 ~character:2 in
let end_ = Position.create ~line:8 ~character:2 in
Range.create ~start ~end_
in
print_code_actions
source
range
~filter:(find_action "destruct-line (enumerate cases, use existing match)");
[%expect
{|
Code actions:
{
"edit": {
"documentChanges": [
{
"edits": [
{
"newText": "A -> _\n | B -> _\n | C -> _\n | D",
"range": {
"end": { "character": 5, "line": 8 },
"start": { "character": 4, "line": 8 }
}
}
],
"textDocument": { "uri": "file:///foo.ml", "version": 0 }
}
]
},
"isPreferred": false,
"kind": "destruct-line (enumerate cases, use existing match)",
"title": "Destruct-line (enumerate cases, use existing match)"
}
|}]
;;
let%expect_test "destruct uses the right number of newlines" =
let source =
{ocaml|
type t =
| Very_long_name_for_for_the_first_case_so_that_merlin_will_use_multiple_lines
| Almost_as_long_name_for_for_the_second_case
| Another_long_name_for_for_the_third_case
;;
let f (x: t) =
match x with
|ocaml}
in
let range =
let start = Position.create ~line:7 ~character:7 in
let end_ = Position.create ~line:7 ~character:7 in
Range.create ~start ~end_
in
print_code_actions
source
range
~filter:(find_action "destruct-line (enumerate cases, use existing match)");
[%expect
{|
Code actions:
{
"edit": {
"documentChanges": [
{
"edits": [
{
"newText": "match x with\n | Very_long_name_for_for_the_first_case_so_that_merlin_will_use_multiple_lines -> _\n | Almost_as_long_name_for_for_the_second_case -> _\n | Another_long_name_for_for_the_third_case -> _",
"range": {
"end": { "character": 14, "line": 7 },
"start": { "character": 2, "line": 7 }
}
}
],
"textDocument": { "uri": "file:///foo.ml", "version": 0 }
}
]
},
"isPreferred": false,
"kind": "destruct-line (enumerate cases, use existing match)",
"title": "Destruct-line (enumerate cases, use existing match)"
}
|}]
;;
let%expect_test "destruct strips parentheses even on long lines" =
let source =
{ocaml|
type q =
| Very_long_name_for_for_the_first_case_so_that_merlin_will_be_forced_to_use_multiple_lines
| Almost_as_long_name_for_for_the_second_case
| Another_long_name_for_for_the_third_case
| Very_long_name_for_for_the_last_case_so_that_we_can_make_sure_we_handle_both_parens_and_line_breaks of int
;;
let f (x: q) =
match x with
| Almost_as_long_name_for_for_the_second_case -> _
|ocaml}
in
let range =
let start = Position.create ~line:9 ~character:22 in
let end_ = Position.create ~line:9 ~character:22 in
Range.create ~start ~end_
in
print_code_actions
source
range
~filter:(find_action "destruct-line (enumerate cases, use existing match)");
[%expect
{|
Code actions:
{
"edit": {
"documentChanges": [
{
"edits": [
{
"newText": "\n | Very_long_name_for_for_the_first_case_so_that_merlin_will_be_forced_to_use_multiple_lines -> _\n | Another_long_name_for_for_the_third_case -> _\n | Very_long_name_for_for_the_last_case_so_that_we_can_make_sure_we_handle_both_parens_and_line_breaks\n _ -> _",
"range": {
"end": { "character": 52, "line": 9 },
"start": { "character": 52, "line": 9 }
}
}
],
"textDocument": { "uri": "file:///foo.ml", "version": 0 }
}
]
},
"isPreferred": false,
"kind": "destruct-line (enumerate cases, use existing match)",
"title": "Destruct-line (enumerate cases, use existing match)"
}
|}]
;;
let%expect_test "can infer module interfaces" =
let impl_source =
{ocaml|
type t = Foo of int | Bar of bool
let f (x : t) = x
|ocaml}
in
let uri = DocumentUri.of_path "foo.ml" in
let prep client = Test.openDocument ~client ~uri ~source:impl_source in
let intf_source = "" in
let range =
let start = Position.create ~line:0 ~character:0 in
let end_ = Position.create ~line:0 ~character:0 in
Range.create ~start ~end_
in
print_code_actions
intf_source
range
~prep
~path:"foo.mli"
~filter:(find_action "inferred_intf");
[%expect
{|
Code actions:
{
"edit": {
"documentChanges": [
{
"edits": [
{
"newText": "type t = Foo of int | Bar of bool\n\nval f : t -> t\n",
"range": {
"end": { "character": 0, "line": 0 },
"start": { "character": 0, "line": 0 }
}
}
],
"textDocument": { "uri": "file:///foo.mli", "version": 0 }
}
]
},
"isPreferred": false,
"kind": "inferred_intf",
"title": "Insert inferred interface"
} |}]
;;
let%expect_test "inferred interface excludes existing names" =
let impl_source =
{ocaml|
type t = Foo of int | Bar of bool
let f (x : t) = x
|ocaml}
in
let uri = DocumentUri.of_path "foo.ml" in
let prep client = Test.openDocument ~client ~uri ~source:impl_source in
let intf_source =
{ocaml|
val f : t -> t
|ocaml}
in
let range =
let start = Position.create ~line:0 ~character:0 in
let end_ = Position.create ~line:0 ~character:0 in
Range.create ~start ~end_
in
print_code_actions
intf_source
range
~prep
~path:"foo.mli"
~filter:(find_action "inferred_intf");
[%expect
{|
Code actions:
{
"edit": {
"documentChanges": [
{
"edits": [
{
"newText": "type t = Foo of int | Bar of bool\n",
"range": {
"end": { "character": 0, "line": 0 },
"start": { "character": 0, "line": 0 }
}
}
],
"textDocument": { "uri": "file:///foo.mli", "version": 0 }
}
]
},
"isPreferred": false,
"kind": "inferred_intf",
"title": "Insert inferred interface"
}
|}]
;;
let%expect_test "update-signatures adds new function args" =
let impl_source =
{ocaml|
type t = Foo of int | Bar of bool
let f (x : t) (d : bool) =
match x with
|Bar x -> x
|Foo _ -> d
|ocaml}
in
let uri = DocumentUri.of_path "foo.ml" in
let prep client = Test.openDocument ~client ~uri ~source:impl_source in
let intf_source =
{ocaml|
type t = Foo of int | Bar of bool
val f : t -> bool
|ocaml}
in
let range =
let start = Position.create ~line:2 ~character:0 in
let end_ = Position.create ~line:2 ~character:0 in
Range.create ~start ~end_
in
print_code_actions
intf_source
range
~prep
~path:"foo.mli"
~filter:(find_action "update_intf");
[%expect
{|
Code actions:
{
"edit": {
"documentChanges": [
{
"edits": [
{
"newText": "val f : t -> bool -> bool\n",
"range": {
"end": { "character": 17, "line": 2 },
"start": { "character": 0, "line": 2 }
}
}
],
"textDocument": { "uri": "file:///foo.mli", "version": 0 }
}
]
},
"isPreferred": false,
"kind": "update_intf",
"title": "Update signature(s) to match implementation"
}
|}]
;;
let%expect_test "update-signatures removes old function args" =
let impl_source =
{ocaml|
let f i s b =
if b then String.length s > i else String.length s < i
|ocaml}
in
let uri = DocumentUri.of_path "foo.ml" in
let prep client = Test.openDocument ~client ~uri ~source:impl_source in
let intf_source =
{ocaml|
val f : int -> string -> 'a list -> bool -> bool
|ocaml}
in
let range =
let start = Position.create ~line:1 ~character:10 in
let end_ = Position.create ~line:1 ~character:10 in
Range.create ~start ~end_
in
print_code_actions
intf_source
range
~prep
~path:"foo.mli"
~filter:(find_action "update_intf");
[%expect
{|
Code actions:
{
"edit": {
"documentChanges": [
{
"edits": [
{
"newText": "val f : int -> string -> bool -> bool\n",
"range": {
"end": { "character": 48, "line": 1 },
"start": { "character": 0, "line": 1 }
}
}
],
"textDocument": { "uri": "file:///foo.mli", "version": 0 }
}
]
},
"isPreferred": false,
"kind": "update_intf",
"title": "Update signature(s) to match implementation"
}
|}]
;;
let%expect_test "update-signatures updates parameter types" =
let impl_source =
{ocaml|
let f i s l b =
if b then List.length s > i else List.length l < i
|ocaml}
in
let uri = DocumentUri.of_path "foo.ml" in
let prep client = Test.openDocument ~client ~uri ~source:impl_source in
let intf_source =
{ocaml|
val f : int -> string -> 'a list -> bool -> bool
|ocaml}
in
let range =
let start = Position.create ~line:1 ~character:1 in
let end_ = Position.create ~line:1 ~character:12 in
Range.create ~start ~end_
in
print_code_actions
intf_source
range
~prep
~path:"foo.mli"
~filter:(find_action "update_intf");
[%expect
{|
Code actions:
{
"edit": {
"documentChanges": [
{
"edits": [
{
"newText": "val f : int -> 'a list -> 'b list -> bool -> bool\n",
"range": {
"end": { "character": 48, "line": 1 },
"start": { "character": 0, "line": 1 }
}
}
],
"textDocument": { "uri": "file:///foo.mli", "version": 0 }
}
]
},
"isPreferred": false,
"kind": "update_intf",
"title": "Update signature(s) to match implementation"
}
|}]
;;
let%expect_test "update-signatures preserves functions and their comments" =
let impl_source =
{ocaml|
let f x = x + 1;;
let g x y z ~another_arg ~yet_another_arg ~keep_them_coming = x - y + z + another_arg + yet_another_arg + keep_them_coming;;
let h x = x *. 2.0;;
|ocaml}
in
let uri = DocumentUri.of_path "foo.ml" in
let prep client = Test.openDocument ~client ~uri ~source:impl_source in
let intf_source =
{ocaml|
val f :
int (* This comment should stay. *)
-> int
val g : int
-> int (* This comment should disappear since the function changes. *)
-> int
(* This comment should stay even though the function changes. *)
val h : int -> bool
|ocaml}
in
let range =
let start = Position.create ~line:1 ~character:0 in
let end_ = Position.create ~line:10 ~character:19 in
Range.create ~start ~end_
in
print_code_actions
intf_source
range
~prep
~path:"foo.mli"
~filter:(find_action "update_intf");
[%expect
{|
Code actions:
{
"edit": {
"documentChanges": [
{
"edits": [
{
"newText": "val g :\n int ->\n int ->\n int ->\n another_arg:int -> yet_another_arg:int -> keep_them_coming:int -> int\n",
"range": {
"end": { "character": 10, "line": 7 },
"start": { "character": 0, "line": 5 }
}
},
{
"newText": "val h : float -> float\n",
"range": {
"end": { "character": 19, "line": 10 },
"start": { "character": 0, "line": 10 }
}
}
],
"textDocument": { "uri": "file:///foo.mli", "version": 0 }
}
]
},
"isPreferred": false,
"kind": "update_intf",
"title": "Update signature(s) to match implementation"
}
|}]
;;
let%expect_test "update-signatures updates modules" =
let impl_source =
{ocaml|
module M = struct
type t =
| I of int
| F of float
;;
let f (x : t) ~long_name_for_an_integer_argument =
match x with
| I i -> i
| F f -> long_name_for_an_integer_argument
;;
end
|ocaml}
in
let uri = DocumentUri.of_path "foo.ml" in
let prep client = Test.openDocument ~client ~uri ~source:impl_source in
let intf_source =
{ocaml|
module M : sig type t = I of int | B of bool end
|ocaml}
in
let range =
let start = Position.create ~line:1 ~character:0 in
let end_ = Position.create ~line:1 ~character:0 in
Range.create ~start ~end_
in
print_code_actions
intf_source
range
~prep
~path:"foo.mli"
~filter:(find_action "update_intf");
[%expect
{|
Code actions:
{
"edit": {
"documentChanges": [
{
"edits": [
{
"newText": "module M :\n sig\n type t = I of int | F of float\n val f : t -> long_name_for_an_integer_argument:int -> int\n end\n",
"range": {
"end": { "character": 48, "line": 1 },
"start": { "character": 0, "line": 1 }
}
}
],
"textDocument": { "uri": "file:///foo.mli", "version": 0 }
}
]
},
"isPreferred": false,
"kind": "update_intf",
"title": "Update signature(s) to match implementation"
}
|}]
;;
let activate_jump client =
let config =
DidChangeConfigurationParams.create
~settings:(`Assoc [ "merlinJumpCodeActions", `Assoc [ "enable", `Bool true ] ])
in
change_config ~client config
;;
let%expect_test "can jump to match target" =
let source =
{ocaml|
type t = Foo of int | Bar of bool
let square x = x * x
let f (x : t) (d : bool) =
match x with
|Bar x -> x
|Foo _ -> d
|ocaml}
in
let range =
let start = Position.create ~line:5 ~character:5 in
let end_ = Position.create ~line:5 ~character:5 in
Range.create ~start ~end_
in
print_code_actions
~prep:activate_jump
source
range
~filter:(find_action "merlin-jump-match");
[%expect
{|
Code actions:
{
"command": {
"arguments": [
"file:///foo.ml",
{
"end": { "character": 2, "line": 4 },
"start": { "character": 2, "line": 4 }
}
],
"command": "ocamllsp/merlin-jump-to-target",
"title": "Match jump"
},
"kind": "merlin-jump-match",
"title": "Match jump"
}
|}]
;;
let%expect_test "can jump to match-next-case target" =
let source =
{ocaml|
type t = Foo of int | Bar of bool
let square x = x * x
let f (x : t) (d : bool) =
match x with
|Bar x -> x
|Foo _ -> d
|ocaml}
in
let range =
let start = Position.create ~line:5 ~character:5 in
let end_ = Position.create ~line:5 ~character:5 in
Range.create ~start ~end_
in
print_code_actions
~prep:activate_jump
source
range
~filter:(find_action "merlin-jump-next-case");
[%expect
{|
Code actions:
{
"command": {
"arguments": [
"file:///foo.ml",
{
"end": { "character": 3, "line": 6 },
"start": { "character": 3, "line": 6 }
}
],
"command": "ocamllsp/merlin-jump-to-target",
"title": "Next-case jump"
},
"kind": "merlin-jump-next-case",
"title": "Next-case jump"
} |}]
;;
let%expect_test "can jump to match-prev-case target" =
let source =
{ocaml|
type t = Foo of int | Bar of bool
let square x = x * x
let f (x : t) (d : bool) =
match x with
|Bar x -> x
|Foo _ -> d
|ocaml}
in
let range =
let start = Position.create ~line:5 ~character:5 in
let end_ = Position.create ~line:5 ~character:5 in
Range.create ~start ~end_
in
print_code_actions
~prep:activate_jump
source
range
~filter:(find_action "merlin-jump-prev-case");
[%expect
{|
Code actions:
{
"command": {
"arguments": [
"file:///foo.ml",
{
"end": { "character": 3, "line": 5 },
"start": { "character": 3, "line": 5 }
}
],
"command": "ocamllsp/merlin-jump-to-target",
"title": "Prev-case jump"
},
"kind": "merlin-jump-prev-case",
"title": "Prev-case jump"
} |}]
;;
let%expect_test "can jump to let target" =
let source =
{ocaml|
type t = Foo of int | Bar of bool
let square x = x * x
let f (x : t) (d : bool) =
match x with
|Bar x -> x
|Foo _ -> d
|ocaml}
in
let range =
let start = Position.create ~line:5 ~character:5 in
let end_ = Position.create ~line:5 ~character:5 in
Range.create ~start ~end_
in
print_code_actions
~prep:activate_jump
source
range
~filter:(find_action "merlin-jump-let");
[%expect
{|
Code actions:
{
"command": {
"arguments": [
"file:///foo.ml",
{
"end": { "character": 0, "line": 3 },
"start": { "character": 0, "line": 3 }
}
],
"command": "ocamllsp/merlin-jump-to-target",
"title": "Let jump"
},
"kind": "merlin-jump-let",
"title": "Let jump"
} |}]
;;
let%expect_test "can jump to fun target" =
let source =
{ocaml|
type t = Foo of int | Bar of bool
let square x = x * x
let f (x : t) (d : bool) =
match x with
|Bar x -> x
|Foo _ -> d
|ocaml}
in
let range =
let start = Position.create ~line:5 ~character:5 in
let end_ = Position.create ~line:5 ~character:5 in
Range.create ~start ~end_
in
print_code_actions
~prep:activate_jump
source
range
~filter:(find_action "merlin-jump-fun");
[%expect
{|
Code actions:
{
"command": {
"arguments": [
"file:///foo.ml",
{
"end": { "character": 0, "line": 3 },
"start": { "character": 0, "line": 3 }
}
],
"command": "ocamllsp/merlin-jump-to-target",
"title": "Fun jump"
},
"kind": "merlin-jump-fun",
"title": "Fun jump"
} |}]
;;
let%expect_test "can jump to module target" =
let source =
{ocaml|
module FooBar = struct
type t = Foo of int | Bar of bool
end
let f (x : t) (d : bool) =
match x with
|Bar x -> x
|Foo _ -> d
|ocaml}
in
let range =
let start = Position.create ~line:2 ~character:5 in
let end_ = Position.create ~line:2 ~character:5 in
Range.create ~start ~end_
in
print_code_actions
~prep:activate_jump
source
range
~filter:(find_action "merlin-jump-module");
[%expect
{|
Code actions:
{
"command": {
"arguments": [
"file:///foo.ml",
{
"end": { "character": 0, "line": 1 },
"start": { "character": 0, "line": 1 }
}
],
"command": "ocamllsp/merlin-jump-to-target",
"title": "Module jump"
},
"kind": "merlin-jump-module",
"title": "Module jump"
} |}]
;;
let%expect_test "can jump to module-type target" =
let source =
{ocaml|
module type ORDER = sig
type t
val leq : t -> t -> bool
val equal : t -> t -> bool
end
let f (x : t) (d : bool) =
match x with
|Bar x -> x
|Foo _ -> d
|ocaml}
in
let range =
let start = Position.create ~line:4 ~character:5 in
let end_ = Position.create ~line:4 ~character:5 in
Range.create ~start ~end_
in
print_code_actions
~prep:activate_jump
source
range
~filter:(find_action "merlin-jump-module-type");
[%expect
{|
Code actions:
{
"command": {
"arguments": [
"file:///foo.ml",
{
"end": { "character": 2, "line": 1 },
"start": { "character": 2, "line": 1 }
}
],
"command": "ocamllsp/merlin-jump-to-target",
"title": "Module-type jump"
},
"kind": "merlin-jump-module-type",
"title": "Module-type jump"
} |}]
;;
let%expect_test "shouldn't find the jump target on the same line" =
let source =
{ocaml|
let square x = x * x
let f (x : t) (d : bool) =
match x with
|Bar x -> x
|Foo _ -> d
|ocaml}
in
let range =
let start = Position.create ~line:0 ~character:5 in
let end_ = Position.create ~line:0 ~character:5 in
Range.create ~start ~end_
in
print_code_actions
~prep:activate_jump
source
range
~filter:(find_action "merlin-jump-fun");
[%expect
{|
No code actions |}]
;;
let position_of_offset src x =
assert (0 <= x && x < String.length src);
let cnum = ref 0
and lnum = ref 0 in
for i = 0 to x - 1 do
if src.[i] = '\n'
then (
incr lnum;
cnum := 0)
else incr cnum
done;
Position.create ~character:!cnum ~line:!lnum
;;
let parse_selection src =
let start_pos =
match String.index src '$' with
| Some x -> x
| None -> failwith "expected a selection opening mark"
in
let end_pos =
match String.index_from src (start_pos + 1) '$' with
| Some x ->
if Option.is_some (String.index_from src (x + 1) '$')
then failwith "unexpected third selection mark";
x - 1 (* account for opening mark *)
| None -> start_pos
in
let start = position_of_offset src start_pos in
let end_ = position_of_offset src end_pos in
let src' =
String.filter_map src ~f:(function
| '$' -> None
| c -> Some c)
in
src', Range.create ~start ~end_
;;
let apply_code_action ?diagnostics title source range =
let open Option.O in
(* collect code action results *)
let code_actions = ref None in
iter_code_actions ?diagnostics ~source range (fun ca -> code_actions := Some ca);
let* m_code_actions = !code_actions in
let* code_actions = m_code_actions in
let* edit =
List.find_map code_actions ~f:(function
| `CodeAction { title = t; edit = Some edit; _ } when t = title -> Some edit
| _ -> None)
in
let+ changes = edit.documentChanges in
List.concat_map changes ~f:(function
| `TextDocumentEdit x ->
List.map x.edits ~f:(function
| `AnnotatedTextEdit (a : AnnotatedTextEdit.t) ->
TextEdit.create ~newText:a.newText ~range:a.range
| `TextEdit e -> e)
| `CreateFile _ | `DeleteFile _ | `RenameFile _ -> [])
|> Test.apply_edits source
;;
let code_action_test ~title source =
let src, range = parse_selection source in
Option.iter (apply_code_action title src range) ~f:print_string
;;