diff --git a/CHANGES.md b/CHANGES.md index 2534ba12d..09bad540b 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,9 @@ +# unreleased + +## Fixes + +- Support for `class`, `class type`, `method` and `property` for `DocumentSymbol` query (#1487 fixes #1449) + # 1.22.0 ## Features diff --git a/ocaml-lsp-server/src/document_symbol.ml b/ocaml-lsp-server/src/document_symbol.ml index 37504a2da..390363ddd 100644 --- a/ocaml-lsp-server/src/document_symbol.ml +++ b/ocaml-lsp-server/src/document_symbol.ml @@ -135,6 +135,63 @@ let module_binding_document_symbol (pmod : Parsetree.module_binding) ~children = () ;; +let visit_class_sig (desc : Parsetree.class_type) = + match desc.pcty_desc with + | Pcty_signature cs -> + List.filter_map + ~f:(fun field -> + match field.pctf_desc with + | Pctf_val (label, _, _, _) -> + DocumentSymbol.create + ~name:label.txt + ~kind:Property + ~range:(Range.of_loc field.pctf_loc) + ~selectionRange:(Range.of_loc label.loc) + () + |> Option.some + | Pctf_method (label, _, _, _) -> + DocumentSymbol.create + ~name:label.txt + ~kind:Method + ~range:(Range.of_loc field.pctf_loc) + ~selectionRange:(Range.of_loc label.loc) + () + |> Option.some + | _ -> None) + cs.pcsig_fields + | _ -> [] +;; + +let class_description_symbol (decl : Parsetree.class_description) = + DocumentSymbol.create + ~name:decl.pci_name.txt + ~kind:Class + ~range:(Range.of_loc decl.pci_loc) + ~selectionRange:(Range.of_loc decl.pci_name.loc) + ~children:(visit_class_sig decl.pci_expr) + () +;; + +let class_declaration_symbol (decl : Parsetree.class_declaration) ~children = + DocumentSymbol.create + ~name:decl.pci_name.txt + ~kind:Class + ~range:(Range.of_loc decl.pci_loc) + ~selectionRange:(Range.of_loc decl.pci_name.loc) + ~children + () +;; + +let class_type_declaration_symbol (decl : Parsetree.class_type_declaration) = + DocumentSymbol.create + ~name:decl.pci_name.txt + ~kind:Interface + ~range:(Range.of_loc decl.pci_loc) + ~selectionRange:(Range.of_loc decl.pci_name.loc) + ~children:(visit_class_sig decl.pci_expr) + () +;; + let binding_document_symbol (binding : Parsetree.value_binding) ~ppx @@ -228,6 +285,10 @@ let symbols_from_parsetree parsetree = descend (fun () -> Ast_iterator.default_iterator.module_type_declaration iterator decl) (module_type_decl_symbol decl) + | Psig_class classes -> + current := !current @ List.map classes ~f:class_description_symbol + | Psig_class_type classes -> + current := !current @ List.map classes ~f:class_type_declaration_symbol | _ -> Ast_iterator.default_iterator.signature_item iterator item in let rec structure_item @@ -257,10 +318,57 @@ let symbols_from_parsetree parsetree = binding_document_symbol binding ~ppx ~is_top_level:true ~children:!current) | Pstr_extension ((name, PStr items), _) -> List.iter items ~f:(fun item -> structure_item ~ppx:(Some name.txt) iterator item) + | Pstr_class classes -> + List.iter + ~f:(fun (klass : Parsetree.class_declaration) -> + descend + (fun () -> + match klass.pci_expr.pcl_desc with + | Pcl_structure cs -> + Ast_iterator.default_iterator.class_structure iterator cs + | _ -> ()) + (class_declaration_symbol klass)) + classes + | Pstr_class_type classes -> + current := !current @ List.map classes ~f:class_type_declaration_symbol | _ -> Ast_iterator.default_iterator.structure_item iterator item in + let class_structure + (iterator : Ast_iterator.iterator) + (item : Parsetree.class_structure) + = + List.iter ~f:(Ast_iterator.default_iterator.class_field iterator) item.pcstr_fields + in + let class_field (iterator : Ast_iterator.iterator) (item : Parsetree.class_field) = + let mk_symbol ?children ~kind (label : string Asttypes.loc) = + DocumentSymbol.create + ~name:label.txt + ~kind + ~range:(Range.of_loc item.pcf_loc) + ~selectionRange:(Range.of_loc label.loc) + ?children + () + in + match item.pcf_desc with + | Pcf_val (label, _, Parsetree.Cfk_virtual _) -> + let symbol = mk_symbol ~kind:Property label in + current := !current @ [ symbol ] + | Pcf_val (label, _, Parsetree.Cfk_concrete (_, expr)) -> + descend + (fun () -> Ast_iterator.default_iterator.expr iterator expr) + (fun ~children -> mk_symbol ~kind:Property label ~children) + | Pcf_method (label, _, Parsetree.Cfk_virtual _) -> + let symbol = mk_symbol ~kind:Method label in + current := !current @ [ symbol ] + | Pcf_method (label, _, Parsetree.Cfk_concrete (_, expr)) -> + descend + (fun () -> Ast_iterator.default_iterator.expr iterator expr) + (fun ~children -> mk_symbol ~kind:Method label ~children) + | _ -> Ast_iterator.default_iterator.class_field iterator item + in let expr (iterator : Ast_iterator.iterator) (item : Parsetree.expression) = match item.pexp_desc with + | Pexp_object cs -> Ast_iterator.default_iterator.class_structure iterator cs | Pexp_let (_, bindings, inner) -> let outer = !current in let bindings = @@ -277,6 +385,8 @@ let symbols_from_parsetree parsetree = { Ast_iterator.default_iterator with signature_item ; structure_item = structure_item ~ppx:None + ; class_structure + ; class_field ; expr } in diff --git a/ocaml-lsp-server/test/e2e-new/document_symbol.ml b/ocaml-lsp-server/test/e2e-new/document_symbol.ml new file mode 100644 index 000000000..86206f6a3 --- /dev/null +++ b/ocaml-lsp-server/test/e2e-new/document_symbol.ml @@ -0,0 +1,537 @@ +open Test.Import + +module Util = struct + let call_document_symbol client = + let uri = DocumentUri.of_path "test.ml" in + let textDocument = TextDocumentIdentifier.create ~uri in + let param = Lsp.Types.DocumentSymbolParams.create ~textDocument () in + let req = Lsp.Client_request.DocumentSymbol param in + Client.request client req + ;; +end + +let print_as_json result = + result |> Yojson.Safe.pretty_to_string ~std:false |> print_endline +;; + +let print_result x = + let result = + match x with + | Some (`DocumentSymbol xs) -> List.map ~f:DocumentSymbol.yojson_of_t xs + | Some (`SymbolInformation xs) -> List.map ~f:SymbolInformation.yojson_of_t xs + | None -> [] + in + print_as_json (`List result) +;; + +let%expect_test "documentOutline in an empty file" = + let source = {||} in + let request client = + let open Fiber.O in + let+ response = Util.call_document_symbol client in + print_result response + in + Helpers.test source request; + [%expect {| [] |}] +;; + +let%expect_test "documentOutline with a module" = + let source = + {| + module T = struct + type t = int + end + |} + in + let request client = + let open Fiber.O in + let+ response = Util.call_document_symbol client in + print_result response + in + Helpers.test source request; + [%expect + {| + [ + { + "kind": 2, + "location": { + "range": { + "end": { "character": 7, "line": 3 }, + "start": { "character": 4, "line": 1 } + }, + "uri": "file:///test.ml" + }, + "name": "T" + }, + { + "containerName": "T", + "kind": 26, + "location": { + "range": { + "end": { "character": 18, "line": 2 }, + "start": { "character": 6, "line": 2 } + }, + "uri": "file:///test.ml" + }, + "name": "t" + } + ] + |}] +;; + +let%expect_test "documentOutline with a module, a class and a class type" = + let source = + {| + module T = struct + type t = int + class type b = object end + end + class c = object end + |} + in + let request client = + let open Fiber.O in + let+ response = Util.call_document_symbol client in + print_result response + in + Helpers.test source request; + [%expect + {| + [ + { + "kind": 2, + "location": { + "range": { + "end": { "character": 8, "line": 4 }, + "start": { "character": 4, "line": 1 } + }, + "uri": "file:///test.ml" + }, + "name": "T" + }, + { + "containerName": "T", + "kind": 26, + "location": { + "range": { + "end": { "character": 17, "line": 2 }, + "start": { "character": 5, "line": 2 } + }, + "uri": "file:///test.ml" + }, + "name": "t" + }, + { + "containerName": "T", + "kind": 11, + "location": { + "range": { + "end": { "character": 30, "line": 3 }, + "start": { "character": 5, "line": 3 } + }, + "uri": "file:///test.ml" + }, + "name": "b" + }, + { + "kind": 5, + "location": { + "range": { + "end": { "character": 25, "line": 5 }, + "start": { "character": 5, "line": 5 } + }, + "uri": "file:///test.ml" + }, + "name": "c" + } + ] + |}] +;; + +let%expect_test "documentOutline with recursive definition" = + let source = + {| + class a = object end and b = object end and c = object end + class type ta = object end and tb = object end + |} + in + let request client = + let open Fiber.O in + let+ response = Util.call_document_symbol client in + print_result response + in + Helpers.test source request; + [%expect + {| + [ + { + "kind": 5, + "location": { + "range": { + "end": { "character": 25, "line": 1 }, + "start": { "character": 5, "line": 1 } + }, + "uri": "file:///test.ml" + }, + "name": "a" + }, + { + "kind": 5, + "location": { + "range": { + "end": { "character": 44, "line": 1 }, + "start": { "character": 26, "line": 1 } + }, + "uri": "file:///test.ml" + }, + "name": "b" + }, + { + "kind": 5, + "location": { + "range": { + "end": { "character": 63, "line": 1 }, + "start": { "character": 45, "line": 1 } + }, + "uri": "file:///test.ml" + }, + "name": "c" + }, + { + "kind": 11, + "location": { + "range": { + "end": { "character": 31, "line": 2 }, + "start": { "character": 5, "line": 2 } + }, + "uri": "file:///test.ml" + }, + "name": "ta" + }, + { + "kind": 11, + "location": { + "range": { + "end": { "character": 51, "line": 2 }, + "start": { "character": 32, "line": 2 } + }, + "uri": "file:///test.ml" + }, + "name": "tb" + } + ] + |}] +;; + +let%expect_test "documentOutline with recursive definition and methods" = + let source = + {| + class a = object end + and b = object + val foo = 10 + method bar () = print_endline "bar" + end and c = object end + class type ta = object + method baz : int -> int -> string + end and tb = object end + |} + in + let request client = + let open Fiber.O in + let+ response = Util.call_document_symbol client in + print_result response + in + Helpers.test source request; + [%expect + {| + [ + { + "kind": 5, + "location": { + "range": { + "end": { "character": 25, "line": 1 }, + "start": { "character": 5, "line": 1 } + }, + "uri": "file:///test.ml" + }, + "name": "a" + }, + { + "kind": 5, + "location": { + "range": { + "end": { "character": 8, "line": 5 }, + "start": { "character": 5, "line": 2 } + }, + "uri": "file:///test.ml" + }, + "name": "b" + }, + { + "containerName": "b", + "kind": 7, + "location": { + "range": { + "end": { "character": 17, "line": 3 }, + "start": { "character": 5, "line": 3 } + }, + "uri": "file:///test.ml" + }, + "name": "foo" + }, + { + "containerName": "b", + "kind": 6, + "location": { + "range": { + "end": { "character": 40, "line": 4 }, + "start": { "character": 5, "line": 4 } + }, + "uri": "file:///test.ml" + }, + "name": "bar" + }, + { + "kind": 5, + "location": { + "range": { + "end": { "character": 27, "line": 5 }, + "start": { "character": 9, "line": 5 } + }, + "uri": "file:///test.ml" + }, + "name": "c" + }, + { + "kind": 11, + "location": { + "range": { + "end": { "character": 8, "line": 8 }, + "start": { "character": 5, "line": 6 } + }, + "uri": "file:///test.ml" + }, + "name": "ta" + }, + { + "containerName": "ta", + "kind": 6, + "location": { + "range": { + "end": { "character": 41, "line": 7 }, + "start": { "character": 8, "line": 7 } + }, + "uri": "file:///test.ml" + }, + "name": "baz" + }, + { + "kind": 11, + "location": { + "range": { + "end": { "character": 28, "line": 8 }, + "start": { "character": 9, "line": 8 } + }, + "uri": "file:///test.ml" + }, + "name": "tb" + } + ] + |}] +;; + +let%expect_test "documentOutline with nested recursive definition and methods" = + let source = + {| + class a = object + val b = object + method inside_a_b () = + val x_inside_a_b = 10 in + print_int x + end + end + and b = object + val foo = 10 + method bar () = print_endline "bar" + end and c = object end + class type ta = object + method baz : int -> int -> string + end and tb = object end + let final_let = + let c = object method foo = 10 end in c + |} + in + let request client = + let open Fiber.O in + let+ response = Util.call_document_symbol client in + print_result response + in + Helpers.test source request; + [%expect + {| + [ + { + "kind": 5, + "location": { + "range": { + "end": { "character": 8, "line": 6 }, + "start": { "character": 5, "line": 1 } + }, + "uri": "file:///test.ml" + }, + "name": "a" + }, + { + "containerName": "a", + "kind": 7, + "location": { + "range": { + "end": { "character": 16, "line": 5 }, + "start": { "character": 5, "line": 2 } + }, + "uri": "file:///test.ml" + }, + "name": "b" + }, + { + "containerName": "b", + "kind": 6, + "location": { + "range": { + "end": { "character": 27, "line": 3 }, + "start": { "character": 5, "line": 3 } + }, + "uri": "file:///test.ml" + }, + "name": "inside_a_b" + }, + { + "containerName": "b", + "kind": 7, + "location": { + "range": { + "end": { "character": 26, "line": 4 }, + "start": { "character": 5, "line": 4 } + }, + "uri": "file:///test.ml" + }, + "name": "x_inside_a_b" + }, + { + "kind": 5, + "location": { + "range": { + "end": { "character": 8, "line": 11 }, + "start": { "character": 5, "line": 8 } + }, + "uri": "file:///test.ml" + }, + "name": "b" + }, + { + "containerName": "b", + "kind": 7, + "location": { + "range": { + "end": { "character": 17, "line": 9 }, + "start": { "character": 5, "line": 9 } + }, + "uri": "file:///test.ml" + }, + "name": "foo" + }, + { + "containerName": "b", + "kind": 6, + "location": { + "range": { + "end": { "character": 40, "line": 10 }, + "start": { "character": 5, "line": 10 } + }, + "uri": "file:///test.ml" + }, + "name": "bar" + }, + { + "kind": 5, + "location": { + "range": { + "end": { "character": 27, "line": 11 }, + "start": { "character": 9, "line": 11 } + }, + "uri": "file:///test.ml" + }, + "name": "c" + }, + { + "kind": 11, + "location": { + "range": { + "end": { "character": 8, "line": 14 }, + "start": { "character": 5, "line": 12 } + }, + "uri": "file:///test.ml" + }, + "name": "ta" + }, + { + "containerName": "ta", + "kind": 6, + "location": { + "range": { + "end": { "character": 41, "line": 13 }, + "start": { "character": 8, "line": 13 } + }, + "uri": "file:///test.ml" + }, + "name": "baz" + }, + { + "kind": 11, + "location": { + "range": { + "end": { "character": 28, "line": 14 }, + "start": { "character": 9, "line": 14 } + }, + "uri": "file:///test.ml" + }, + "name": "tb" + }, + { + "kind": 13, + "location": { + "range": { + "end": { "character": 46, "line": 16 }, + "start": { "character": 5, "line": 15 } + }, + "uri": "file:///test.ml" + }, + "name": "final_let" + }, + { + "containerName": "final_let", + "kind": 13, + "location": { + "range": { + "end": { "character": 41, "line": 16 }, + "start": { "character": 7, "line": 16 } + }, + "uri": "file:///test.ml" + }, + "name": "c" + }, + { + "containerName": "c", + "kind": 6, + "location": { + "range": { + "end": { "character": 37, "line": 16 }, + "start": { "character": 22, "line": 16 } + }, + "uri": "file:///test.ml" + }, + "name": "foo" + } + ] + |}] +;; diff --git a/ocaml-lsp-server/test/e2e-new/dune b/ocaml-lsp-server/test/e2e-new/dune index add8bed2a..5c2464bb3 100644 --- a/ocaml-lsp-server/test/e2e-new/dune +++ b/ocaml-lsp-server/test/e2e-new/dune @@ -63,6 +63,7 @@ test type_enclosing documentation + document_symbol merlin_jump type_search with_pp