mirror of
https://github.com/c-cube/linol.git
synced 2025-12-08 12:15:50 -05:00
1712 lines
42 KiB
OCaml
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
|
|
;;
|