Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Jump Custom Requests #1361

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions ocaml-lsp-server/docs/ocamllsp/jump-spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Jump Request

## Description

This custom request allows Merlin-type code navigation in a source buffer.

## Server capability

- propert name: `handleJump`
- property type: `boolean`

## Request

```js
export interface JumpParams extends TextDocumentPositionParams
{
target: string;
}
```

- method: `ocamllsp/jump`
- params:
- `TextDocumentIdentifier`: Specifies the document for which the request is sent. It includes a uri property that points to the document.
- `Position`: Specifies the position in the document for which the documentation is requested. It includes line and character properties.
More details can be found in the [TextDocumentPositionParams - LSP Specification](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocumentPositionParams).
- `Target`: A string representing the identifier within the document to search for and jump to.

## Response

```js
result: Jump | String
export interface Jump extends Location {
uri: string;
range: {
start: Position;
end: Position;
}
}
```

- result:
- Type: Jump or string
- Description: If the jump is successful, a list of locations is returned where the first location is the most relevant one. If no relevant jump location is found, the result will be a string "no matching target" or an error message.
- Jump:
- Type: Location[], A list of Location objects representing the potential targets of the jump.
- Location:
- uri: The URI of the document where the jump target is located.
- range: The range within the document where the jump target is located. Both start and end positions are the same as the jump target location.
The Location type is the same type returned by `Goto` requests such as `goto-definition`, `goto-declaration` and `goto-typeDefinition`.
1 change: 1 addition & 0 deletions ocaml-lsp-server/src/custom_requests/custom_request.ml
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ module Typed_holes = Req_typed_holes
module Type_enclosing = Req_type_enclosing
module Wrapping_ast_node = Req_wrapping_ast_node
module Get_documentation = Req_get_documentation
module Merlin_jump = Req_jump
1 change: 1 addition & 0 deletions ocaml-lsp-server/src/custom_requests/custom_request.mli
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ module Typed_holes = Req_typed_holes
module Type_enclosing = Req_type_enclosing
module Wrapping_ast_node = Req_wrapping_ast_node
module Get_documentation = Req_get_documentation
module Merlin_jump = Req_jump
84 changes: 84 additions & 0 deletions ocaml-lsp-server/src/custom_requests/req_jump.ml
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
open Import
module TextDocumentPositionParams = Lsp.Types.TextDocumentPositionParams

let meth = "ocamllsp/jump"
let capability = "handleJump", `Bool true

module JumpParams = struct
type t =
{ text_document : TextDocumentIdentifier.t
; position : Position.t
; target : string
}

let t_of_yojson json =
let open Yojson.Safe.Util in
let textDocumentPosition = TextDocumentPositionParams.t_of_yojson json in
let target = json |> member "target" |> to_string in
{ position = textDocumentPosition.position
; text_document = textDocumentPosition.textDocument
; target
}
;;

let yojson_of_t { text_document; position; target } =
`Assoc
[ "textDocument", TextDocumentIdentifier.yojson_of_t text_document
; "position", Position.yojson_of_t position
; "target", `String target
]
;;
end

module Jump = struct
type t = [ `Location of Location.t list ]

let yojson_of_t t = `List (List.map ~f:Location.yojson_of_t t)

let t_of_yojson json =
match json with
| `List lst ->
let locations = List.map ~f:Location.t_of_yojson lst in
`Location locations
| _ -> failwith "Invalid JSON for Jump.t"
;;
end

type t = Jump.t

let t_of_yojson json = Jump.t_of_yojson json

module Request_params = struct
type t = JumpParams.t

let yojson_of_t t = JumpParams.yojson_of_t t
let create ~text_document ~position ~target () : t = { text_document; position; target }
end

let dispatch ~merlin ~position ~target =
Document.Merlin.with_pipeline_exn merlin (fun pipeline ->
let pposition = Position.logical position in
let query = Query_protocol.Jump (target, pposition) in
Query_commands.dispatch pipeline query)
;;

let on_request ~params state =
Fiber.of_thunk (fun () ->
let params = (Option.value ~default:(`Assoc []) params :> Yojson.Safe.t) in
let JumpParams.{ text_document; position; target } = JumpParams.t_of_yojson params in
let uri = text_document.uri in
let doc = Document_store.get state.State.store uri in
match Document.kind doc with
| `Other -> Fiber.return `Null
| `Merlin merlin ->
Fiber.bind (dispatch ~merlin ~position ~target) ~f:(fun res ->
match res with
| `Error err -> Fiber.return (`String err)
| `Found pos ->
(match Position.of_lexical_position pos with
| None -> Fiber.return `Null
| Some position ->
let range = { Range.start = position; end_ = position } in
let locs = [ { Location.range; uri } ] in
Fiber.return (Jump.yojson_of_t locs))))
;;
21 changes: 21 additions & 0 deletions ocaml-lsp-server/src/custom_requests/req_jump.mli
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
open Import

module Request_params : sig
type t

val yojson_of_t : t -> Json.t

val create
: text_document:TextDocumentIdentifier.t
-> position:Position.t
-> target:string
-> unit
-> t
end

type t

val t_of_yojson : Json.t -> t
val meth : string
val capability : string * [> `Bool of bool ]
val on_request : params:Jsonrpc.Structured.t option -> State.t -> Json.t Fiber.t
1 change: 1 addition & 0 deletions ocaml-lsp-server/src/ocaml_lsp_server.ml
Original file line number Diff line number Diff line change
Expand Up @@ -537,6 +537,7 @@ let on_request
; Req_merlin_call_compatible.meth, Req_merlin_call_compatible.on_request
; Req_type_enclosing.meth, Req_type_enclosing.on_request
; Req_get_documentation.meth, Req_get_documentation.on_request
; Req_jump.meth, Req_jump.on_request
; Req_wrapping_ast_node.meth, Req_wrapping_ast_node.on_request
; ( Semantic_highlighting.Debug.meth_request_full
, Semantic_highlighting.Debug.on_request_full )
Expand Down
1 change: 1 addition & 0 deletions ocaml-lsp-server/test/e2e-new/dune
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
test
type_enclosing
documentation
merlin_jump
with_pp
with_ppx
workspace_change_config))))
105 changes: 105 additions & 0 deletions ocaml-lsp-server/test/e2e-new/merlin_jump.ml
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
open Test.Import
module Req = Ocaml_lsp_server.Custom_request.Merlin_jump

module Util = struct
let call_jump position target client =
let uri = DocumentUri.of_path "test.ml" in
let text_document = TextDocumentIdentifier.create ~uri in
let params =
Req.Request_params.create ~text_document ~position ~target ()
|> Req.Request_params.yojson_of_t
|> Jsonrpc.Structured.t_of_yojson
|> Option.some
in
let req = Lsp.Client_request.UnknownRequest { meth = "ocamllsp/jump"; params } in
Client.request client req
;;

let test ~line ~character ~target ~source =
let position = Position.create ~character ~line in
let request client =
let open Fiber.O in
let+ response = call_jump position target client in
Test.print_result response
in
Helpers.test source request
;;
end

let%expect_test "Get location of the next match case" =
let source =
{|
let find_vowel x =
match x with
| 'A' -> true
| 'E' -> true
| 'I' -> true
| 'O' -> true
| 'U' -> true
| _ -> false
|}
in
let line = 3 in
let character = 2 in
let target = "match-next-case" in
Util.test ~line ~character ~target ~source;
[%expect {|
[
{
"range": {
"end": { "character": 2, "line": 4 },
"start": { "character": 2, "line": 4 }
},
"uri": "file:///test.ml"
}
] |}]
;;

let%expect_test "Get location of a the module" =
let source =
{|type a = Foo | Bar

module A = struct
let f () = 10
let g = Bar
let h x = x

module B = struct
type b = Baz

let x = (Baz, 10)
let y = (Bar, Foo)
end

type t = { a : string; b : float }

let z = { a = "Hello"; b = 1.0 }
end|}
in
let line = 10 in
let character = 3 in
let target = "module" in
Util.test ~line ~character ~target ~source;
[%expect {|
[
{
"range": {
"end": { "character": 2, "line": 7 },
"start": { "character": 2, "line": 7 }
},
"uri": "file:///test.ml"
}
] |}]
;;


let%expect_test "Same line should output no locations" =
let source =
{|let x = 5 |}
in
let line = 1 in
let character = 5 in
let target = "let" in
Util.test ~line ~character ~target ~source;
[%expect {| "No matching target" |}]
;;
Loading