Skip to content

Commit b363a35

Browse files
PizieDustvoodoos
andauthored
Jump Custom Request (#1374)
Co-authored-by: Ulysse Gérard <[email protected]>
1 parent 3b5d4e8 commit b363a35

File tree

10 files changed

+319
-1
lines changed

10 files changed

+319
-1
lines changed

CHANGES.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55

66
- Make MerlinJump code action configurable (#1376)
77

8+
- Add custom [`ocamllsp/jump`](/ocaml-lsp-server/docs/ocamllsp/merlinJump-spec.md) request (#1374)
9+
810
## Fixes
911

1012
- Fix fd leak in running external processes for preprocessing (#1349)
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# Merlin Jump Request
2+
3+
## Description
4+
5+
This custom request allows Merlin-type code navigation in a source buffer.
6+
7+
## Server capability
8+
9+
- propert name: `handleJump`
10+
- property type: `boolean`
11+
12+
## Request
13+
14+
- method: `ocamllsp/jump`
15+
- params: `JumpParams` extends [TextDocumentPositionParams](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocumentPositionParams) and is defined as follows:
16+
17+
```js
18+
export interface JumpParams extends TextDocumentPositionParams
19+
{
20+
/**
21+
* The requested target of the jump, one of `fun`, `let`, `module`,
22+
* `module-type`, `match`, `match-next-case`, `match-prev-case`.
23+
*
24+
* If omitted, all valid targets will be considered.
25+
*/
26+
target?: string;
27+
}
28+
```
29+
30+
## Response
31+
32+
- result: `Jump`
33+
34+
```js
35+
36+
export interface TargetPosition {
37+
/**
38+
* The target's kind.
39+
*/
40+
target: string;
41+
42+
/**
43+
* The corresponding position in the request's document.
44+
*/
45+
position: Position;
46+
}
47+
48+
export interface Jump {
49+
/**
50+
* The list of possible targets to jump-to.
51+
*/
52+
jumps: TargetPosition[]
53+
}
54+
```

ocaml-lsp-server/src/custom_requests/custom_request.ml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@ module Type_enclosing = Req_type_enclosing
88
module Wrapping_ast_node = Req_wrapping_ast_node
99
module Get_documentation = Req_get_documentation
1010
module Type_search = Req_type_search
11+
module Merlin_jump = Req_merlin_jump

ocaml-lsp-server/src/custom_requests/custom_request.mli

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@ module Type_enclosing = Req_type_enclosing
1010
module Wrapping_ast_node = Req_wrapping_ast_node
1111
module Get_documentation = Req_get_documentation
1212
module Type_search = Req_type_search
13+
module Merlin_jump = Req_merlin_jump
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
open Import
2+
module TextDocumentPositionParams = Lsp.Types.TextDocumentPositionParams
3+
4+
let meth = "ocamllsp/jump"
5+
let capability = "handleJump", `Bool true
6+
7+
module JumpParams = struct
8+
let targets =
9+
[ "fun"
10+
; "match"
11+
; "let"
12+
; "module"
13+
; "module-type"
14+
; "match-next-case"
15+
; "match-prev-case"
16+
]
17+
;;
18+
19+
type t =
20+
{ textDocument : TextDocumentIdentifier.t
21+
; position : Position.t
22+
; target : string option
23+
}
24+
25+
let t_of_yojson json =
26+
let open Yojson.Safe.Util in
27+
{ textDocument = json |> member "textDocument" |> TextDocumentIdentifier.t_of_yojson
28+
; position = json |> member "position" |> Position.t_of_yojson
29+
; target = json |> member "target" |> to_string_option
30+
}
31+
;;
32+
33+
let yojson_of_t { textDocument; position; target } =
34+
let target =
35+
Option.value_map target ~default:[] ~f:(fun v -> [ "target", `String v ])
36+
in
37+
`Assoc
38+
(("textDocument", TextDocumentIdentifier.yojson_of_t textDocument)
39+
:: ("position", Position.yojson_of_t position)
40+
:: target)
41+
;;
42+
end
43+
44+
module Jump = struct
45+
type t = (string * Position.t) list
46+
47+
let yojson_of_t (lst : t) : Yojson.Safe.t =
48+
let jumps =
49+
List.map
50+
~f:(fun (target, position) ->
51+
`Assoc [ "target", `String target; "position", Position.yojson_of_t position ])
52+
lst
53+
in
54+
`Assoc [ "jumps", `List jumps ]
55+
;;
56+
end
57+
58+
type t = Jump.t
59+
60+
module Request_params = struct
61+
type t = JumpParams.t
62+
63+
let yojson_of_t t = JumpParams.yojson_of_t t
64+
65+
let create ~uri ~position ~target =
66+
{ JumpParams.textDocument = TextDocumentIdentifier.create ~uri; position; target }
67+
;;
68+
end
69+
70+
let dispatch ~merlin ~position ~target =
71+
Document.Merlin.with_pipeline_exn merlin (fun pipeline ->
72+
let pposition = Position.logical position in
73+
let query = Query_protocol.Jump (target, pposition) in
74+
Query_commands.dispatch pipeline query)
75+
;;
76+
77+
let on_request ~params state =
78+
let open Fiber.O in
79+
Fiber.of_thunk (fun () ->
80+
let params = (Option.value ~default:(`Assoc []) params :> Yojson.Safe.t) in
81+
let params = JumpParams.t_of_yojson params in
82+
let uri = params.textDocument.uri in
83+
let position = params.position in
84+
let doc = Document_store.get state.State.store uri in
85+
match Document.kind doc with
86+
| `Other -> Fiber.return `Null
87+
| `Merlin merlin ->
88+
let targets =
89+
match params.target with
90+
| None -> JumpParams.targets
91+
| Some target -> [ target ]
92+
in
93+
let+ results =
94+
Fiber.parallel_map targets ~f:(fun target ->
95+
dispatch ~merlin ~position ~target
96+
|> Fiber.map ~f:(function
97+
| `Error _ -> None
98+
| `Found pos ->
99+
(match Position.of_lexical_position pos with
100+
| None -> None
101+
| Some position -> Some (target, position))))
102+
in
103+
Jump.yojson_of_t (List.filter_map results ~f:Fun.id))
104+
;;
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
open Import
2+
3+
module Request_params : sig
4+
type t
5+
6+
val yojson_of_t : t -> Json.t
7+
val create : uri:DocumentUri.t -> position:Position.t -> target:string option -> t
8+
end
9+
10+
type t
11+
12+
val meth : string
13+
val capability : string * [> `Bool of bool ]
14+
val on_request : params:Jsonrpc.Structured.t option -> State.t -> Json.t Fiber.t

ocaml-lsp-server/src/ocaml_lsp_server.ml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ let initialize_info (client_capabilities : ClientCapabilities.t) : InitializeRes
9797
; Req_get_documentation.capability
9898
; Req_construct.capability
9999
; Req_type_search.capability
100+
; Req_merlin_jump.capability
100101
] )
101102
]
102103
in
@@ -526,6 +527,7 @@ let on_request
526527
; Req_merlin_call_compatible.meth, Req_merlin_call_compatible.on_request
527528
; Req_type_enclosing.meth, Req_type_enclosing.on_request
528529
; Req_get_documentation.meth, Req_get_documentation.on_request
530+
; Req_merlin_jump.meth, Req_merlin_jump.on_request
529531
; Req_wrapping_ast_node.meth, Req_wrapping_ast_node.on_request
530532
; Req_type_search.meth, Req_type_search.on_request
531533
; Req_construct.meth, Req_construct.on_request

ocaml-lsp-server/test/e2e-new/dune

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@
6363
test
6464
type_enclosing
6565
documentation
66+
merlin_jump
6667
type_search
6768
with_pp
6869
with_ppx
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
open Test.Import
2+
module Req = Ocaml_lsp_server.Custom_request.Merlin_jump
3+
4+
module Util = struct
5+
let call_jump position ?target client =
6+
let uri = DocumentUri.of_path "test.ml" in
7+
let params =
8+
Req.Request_params.create ~uri ~position ~target
9+
|> Req.Request_params.yojson_of_t
10+
|> Jsonrpc.Structured.t_of_yojson
11+
|> Option.some
12+
in
13+
let req = Lsp.Client_request.UnknownRequest { meth = "ocamllsp/jump"; params } in
14+
Client.request client req
15+
;;
16+
17+
let test ~line ~character ~source ?target () =
18+
let position = Position.create ~character ~line in
19+
let request client =
20+
let open Fiber.O in
21+
let+ response = call_jump position client ?target in
22+
Test.print_result response
23+
in
24+
Helpers.test source request
25+
;;
26+
end
27+
28+
let%expect_test "Get all jumps including the next match case" =
29+
let source =
30+
{|
31+
let find_vowel x =
32+
match x with
33+
| 'A' -> true
34+
| 'E' -> true
35+
| 'I' -> true
36+
| 'O' -> true
37+
| 'U' -> true
38+
| _ -> false
39+
|}
40+
in
41+
let line = 3 in
42+
let character = 2 in
43+
Util.test ~line ~character ~source ();
44+
[%expect
45+
{|
46+
{
47+
"jumps": [
48+
{ "target": "fun", "position": { "character": 0, "line": 1 } },
49+
{ "target": "match", "position": { "character": 0, "line": 2 } },
50+
{ "target": "let", "position": { "character": 0, "line": 1 } },
51+
{
52+
"target": "match-next-case",
53+
"position": { "character": 2, "line": 4 }
54+
}
55+
]
56+
} |}]
57+
;;
58+
59+
let%expect_test "Get location of the next match case" =
60+
let source =
61+
{|
62+
let find_vowel x =
63+
match x with
64+
| 'A' -> true
65+
| 'E' -> true
66+
| 'I' -> true
67+
| 'O' -> true
68+
| 'U' -> true
69+
| _ -> false
70+
|}
71+
in
72+
let line = 3 in
73+
let character = 2 in
74+
Util.test ~line ~character ~source ~target:"match-next-case" ();
75+
[%expect
76+
{|
77+
{
78+
"jumps": [
79+
{
80+
"target": "match-next-case",
81+
"position": { "character": 2, "line": 4 }
82+
}
83+
]
84+
}
85+
|}]
86+
;;
87+
88+
let%expect_test "Get location of a the module" =
89+
let source =
90+
{|type a = Foo | Bar
91+
92+
module A = struct
93+
let f () = 10
94+
let g = Bar
95+
let h x = x
96+
97+
module B = struct
98+
type b = Baz
99+
100+
let x = (Baz, 10)
101+
let y = (Bar, Foo)
102+
end
103+
104+
type t = { a : string; b : float }
105+
106+
let z = { a = "Hello"; b = 1.0 }
107+
end|}
108+
in
109+
let line = 10 in
110+
let character = 3 in
111+
Util.test ~line ~character ~source ();
112+
[%expect
113+
{|
114+
{
115+
"jumps": [
116+
{ "target": "module", "position": { "character": 2, "line": 7 } }
117+
]
118+
} |}]
119+
;;
120+
121+
let%expect_test "Same line should output no locations" =
122+
let source = {|let x = 5 |} in
123+
let line = 1 in
124+
let character = 5 in
125+
Util.test ~line ~character ~source ();
126+
[%expect {| { "jumps": [] } |}]
127+
;;
128+
129+
let%expect_test "Ask for a non-existing target" =
130+
let source = {|
131+
let find_vowel x = ()
132+
|} in
133+
let line = 1 in
134+
let character = 2 in
135+
Util.test ~line ~character ~source ~target:"notatarget" ();
136+
[%expect
137+
{| { "jumps": [] } |}]
138+
;;

ocaml-lsp-server/test/e2e-new/start_stop.ml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,8 @@ let%expect_test "start/stop" =
9494
"handleTypeEnclosing": true,
9595
"handleGetDocumentation": true,
9696
"handleConstruct": true,
97-
"handleTypeSearch": true
97+
"handleTypeSearch": true,
98+
"handleJump": true
9899
}
99100
},
100101
"foldingRangeProvider": true,

0 commit comments

Comments
 (0)