Skip to content

Commit 997fa11

Browse files
xvwvoodoos
authored andcommitted
More control on typed holes (for navigation) (ocaml#1516)
* Introduce a new custom request `ocamllsp/jumpTypedHole` * Test the custom request * Add CHANGE entry * Add more tests related to range * Add specification * Simplify request description * Refactor typed-holes usage in a dedicated module * Rename request name * Rephrase documentation * Rename `name` to `pipeline_name`
1 parent 7b584d8 commit 997fa11

File tree

13 files changed

+510
-4
lines changed

13 files changed

+510
-4
lines changed

CHANGES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
## Features
44

55
- Make `inlay-hint` for function parameters configurable (#1515)
6+
- Add custom `ocamllsp/jumpToTypedHole` to navigate through typed holes (#1516)
67

78
## Fixes
89

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# Jump To Typed Holes Requet
2+
3+
### Description
4+
5+
Returns the next or previous typed hole at a given position (included
6+
in a range or not).
7+
8+
9+
### Why this custom request needed
10+
11+
Reduces the need for editor-side logic and works well with the
12+
expression construct command. For example, constructing a value of
13+
type `int option` constructs the following expression:
14+
`Some _`, coupled with typed hole navigation, you can move the cursor
15+
directly over the generated hole.
16+
17+
18+
### A note on stability:
19+
20+
> OCaml-LSP does not guarantee stability for this custom request,
21+
> meaning the core contributors may change or remove this custom
22+
> request, as they see necessary.
23+
24+
## Client capability
25+
26+
N/A
27+
28+
## Server capability
29+
30+
- property name: `handleJumpToTypedHole`
31+
- property type: `boolean`
32+
33+
## Request
34+
35+
- method: `ocamllsp/jumpToTypedHole`
36+
- params:
37+
38+
```json
39+
{
40+
"uri": TextDocumentIdentifier,
41+
"position": Position,
42+
"direction": <"next"|"prev">,
43+
"range?": Range
44+
}
45+
```
46+
47+
If a `range` is given, it will only select holes present in the given
48+
range.
49+
50+
## Response
51+
52+
```json
53+
Range | null
54+
```
55+
56+
Returns the next or previous typed hole at a given position. An
57+
optional range can be used to restrict the search.

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ module Infer_intf = Req_infer_intf
44
module Merlin_call_compatible = Req_merlin_call_compatible
55
module Switch_impl_intf = Req_switch_impl_intf
66
module Typed_holes = Req_typed_holes
7+
module Jump_to_typed_hole = Req_jump_to_typed_hole
78
module Type_enclosing = Req_type_enclosing
89
module Wrapping_ast_node = Req_wrapping_ast_node
910
module Get_documentation = Req_get_documentation

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ module Infer_intf = Req_infer_intf
66
module Merlin_call_compatible = Req_merlin_call_compatible
77
module Switch_impl_intf = Req_switch_impl_intf
88
module Typed_holes = Req_typed_holes
9+
module Jump_to_typed_hole = Req_jump_to_typed_hole
910
module Type_enclosing = Req_type_enclosing
1011
module Wrapping_ast_node = Req_wrapping_ast_node
1112
module Get_documentation = Req_get_documentation
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
open Import
2+
3+
let capability = "handleJumpToTypedHole", `Bool true
4+
let meth = "ocamllsp/jumpToTypedHole"
5+
6+
module Request_params = struct
7+
type t =
8+
{ text_document : TextDocumentIdentifier.t
9+
; position : Position.t
10+
; range : Range.t option
11+
; direction : [ `Next | `Prev ]
12+
}
13+
14+
let create ?(direction = `Next) ?range ~text_document ~position () =
15+
{ text_document; position; direction; range }
16+
;;
17+
18+
let yojson_of_direction = function
19+
| `Next -> `String "next"
20+
| `Prev -> `String "prev"
21+
;;
22+
23+
let yojson_of_t { text_document; position; direction; range } =
24+
match TextDocumentIdentifier.yojson_of_t text_document with
25+
| `Assoc assoc ->
26+
let position = "position", Position.yojson_of_t position in
27+
let range =
28+
( "range"
29+
, match range with
30+
| None -> `Null
31+
| Some r -> Range.yojson_of_t r )
32+
in
33+
let direction = "direction", yojson_of_direction direction in
34+
`Assoc (direction :: position :: range :: assoc)
35+
| _ -> (* unreachable *) assert false
36+
;;
37+
38+
let direction_of_yojson json =
39+
let open Yojson.Safe.Util in
40+
let dir = json |> member "direction" |> to_string in
41+
match String.lowercase_ascii dir with
42+
| "prev" -> `Prev
43+
| _ -> `Next
44+
;;
45+
46+
let t_of_yojson json =
47+
let open Yojson.Safe.Util in
48+
let text_document = TextDocumentIdentifier.t_of_yojson json in
49+
let position = json |> member "position" |> Position.t_of_yojson in
50+
let direction = direction_of_yojson json in
51+
let range = json |> member "range" |> to_option Range.t_of_yojson in
52+
{ text_document; position; direction; range }
53+
;;
54+
end
55+
56+
type t = Range.t option
57+
58+
let t_of_yojson opt =
59+
let open Yojson.Safe.Util in
60+
to_option Range.t_of_yojson opt
61+
;;
62+
63+
let yojson_of_t = function
64+
| None -> `Null
65+
| Some range -> Range.yojson_of_t range
66+
;;
67+
68+
let on_request ~(params : Jsonrpc.Structured.t option) (state : State.t) =
69+
Fiber.of_thunk (fun () ->
70+
let params = (Option.value ~default:(`Assoc []) params :> Json.t) in
71+
let Request_params.{ text_document = { uri }; position; direction; range } =
72+
Request_params.t_of_yojson params
73+
in
74+
match Document_store.get_opt state.store uri with
75+
| Some doc ->
76+
let open Fiber.O in
77+
let merlin = Document.merlin_exn doc in
78+
let+ holes = Typed_hole.all ~pipeline_name:"jump-to-typed-hole" merlin in
79+
holes |> Typed_hole.find ~position ~range ~direction |> yojson_of_t
80+
| None ->
81+
Jsonrpc.Response.Error.raise
82+
@@ Jsonrpc.Response.Error.make
83+
~code:Jsonrpc.Response.Error.Code.InvalidParams
84+
~message:
85+
(Printf.sprintf
86+
"Document %s wasn't found in the document store"
87+
(Uri.to_string uri))
88+
())
89+
;;
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
open Import
2+
3+
module Request_params : sig
4+
type t
5+
6+
val create
7+
: ?direction:[ `Next | `Prev ]
8+
-> ?range:Range.t
9+
-> text_document:Lsp.Types.TextDocumentIdentifier.t
10+
-> position:Position.t
11+
-> unit
12+
-> t
13+
14+
val yojson_of_t : t -> Json.t
15+
end
16+
17+
type t
18+
19+
val t_of_yojson : Json.t -> t
20+
val capability : string * Json.t
21+
val meth : string
22+
val on_request : params:Jsonrpc.Structured.t option -> State.t -> Json.t Fiber.t

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

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,7 @@ end
4646

4747
type t = Range.t list
4848

49-
let yojson_of_t holes =
50-
Json.yojson_of_list (fun (loc, _type) -> loc |> Range.of_loc |> Range.yojson_of_t) holes
51-
;;
49+
let yojson_of_t holes = Json.yojson_of_list Range.yojson_of_t holes
5250

5351
let t_of_yojson list =
5452
let open Yojson.Safe.Util in
@@ -72,7 +70,7 @@ let on_request ~(params : Jsonrpc.Structured.t option) (state : State.t) =
7270
()
7371
| Some doc ->
7472
let+ holes =
75-
Document.Merlin.dispatch_exn ~name:"typed-holes" (Document.merlin_exn doc) Holes
73+
Typed_hole.all ~pipeline_name:"typed-holes" (Document.merlin_exn doc)
7674
in
7775
yojson_of_t holes)
7876
;;

ocaml-lsp-server/src/ocaml_lsp_server.ml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ let initialize_info (client_capabilities : ClientCapabilities.t) : InitializeRes
8989
; Req_switch_impl_intf.capability
9090
; Req_infer_intf.capability
9191
; Req_typed_holes.capability
92+
; Req_jump_to_typed_hole.capability
9293
; Req_wrapping_ast_node.capability
9394
; Dune.view_promotion_capability
9495
; Req_hover_extended.capability
@@ -497,6 +498,7 @@ let on_request
497498
Fiber.return (Req_switch_impl_intf.on_request ~params state)) )
498499
; Req_infer_intf.meth, Req_infer_intf.on_request
499500
; Req_typed_holes.meth, Req_typed_holes.on_request
501+
; Req_jump_to_typed_hole.meth, Req_jump_to_typed_hole.on_request
500502
; Req_merlin_call_compatible.meth, Req_merlin_call_compatible.on_request
501503
; Req_type_enclosing.meth, Req_type_enclosing.on_request
502504
; Req_get_documentation.meth, Req_get_documentation.on_request

ocaml-lsp-server/src/typed_hole.ml

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
open Import
2+
3+
let in_range range holes =
4+
match range with
5+
| None -> holes
6+
| Some range -> List.filter ~f:(Range.contains range) holes
7+
;;
8+
9+
let find_prev ~range ~position holes =
10+
let holes = in_range range holes in
11+
Base.List.fold_until
12+
~init:None
13+
~f:(fun prev hole ->
14+
match Position.compare hole.end_ position with
15+
| Lt -> Continue (Some hole)
16+
| Gt | Eq -> Stop prev)
17+
~finish:Fun.id
18+
holes
19+
|> function
20+
| None -> Base.List.last holes
21+
| hole -> hole
22+
;;
23+
24+
let find_next ~range ~position holes =
25+
let holes = in_range range holes in
26+
List.find
27+
~f:(fun hole ->
28+
match Position.compare hole.start position with
29+
| Gt -> true
30+
| Lt | Eq -> false)
31+
holes
32+
|> function
33+
| None -> Base.List.hd holes
34+
| hole -> hole
35+
;;
36+
37+
let find ~range ~position ~direction holes =
38+
match direction with
39+
| `Prev -> find_prev ~range ~position holes
40+
| `Next -> find_next ~range ~position holes
41+
;;
42+
43+
let all ?(pipeline_name = "typed-holes") merlin =
44+
Holes
45+
|> Document.Merlin.dispatch_exn ~name:pipeline_name merlin
46+
|> Fiber.map ~f:(List.map ~f:(fun (loc, _ty) -> Range.of_loc loc))
47+
;;
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
val find
2+
: range:Range.t option
3+
-> position:Position.t
4+
-> direction:[< `Next | `Prev ]
5+
-> Range.t list
6+
-> Range.t option
7+
8+
val all : ?pipeline_name:string -> Document.Merlin.t -> Range.t list Fiber.t

0 commit comments

Comments
 (0)