diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 78bad0e36dd..4c09b1d6b5e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -66,12 +66,13 @@ jobs: exe-suffix: ".exe" dune-profile: release + # Disable for now. eio and eio_main require ocaml >= 5.2.0 # Verify that the compiler still builds with the oldest OCaml version we support. - - os: ubuntu-24.04 - ocaml_compiler: ocaml-variants.5.0.0+options,ocaml-option-static - node-target: linux-x64 - rust-target: x86_64-unknown-linux-musl - dune-profile: static + # - os: ubuntu-24.04 + # ocaml_compiler: ocaml-variants.5.0.0+options,ocaml-option-static + # node-target: linux-x64 + # rust-target: x86_64-unknown-linux-musl + # dune-profile: static runs-on: ${{matrix.os}} @@ -107,7 +108,7 @@ jobs: # https://github.com/ocaml/setup-ocaml/blob/2f57267f071bc8547dfcb9433ff21d44fffef190/packages/setup-ocaml/src/unix.ts#L48 # plus OPAM wants cmake packages: bubblewrap darcs g++-multilib gcc-multilib mercurial musl-tools rsync cmake - version: v4 + version: v5 - name: Restore rewatch build cache id: rewatch-build-cache @@ -176,6 +177,56 @@ jobs: C:\.opam key: ${{ env.opam_cache_key }} + # The static OCaml switch uses musl-gcc. linux-libc-dev installs Linux + # headers under /usr/include, but musl-gcc searches the musl include dir. + # Link the Linux headers into musl's include path so packages with C stubs + # such as uring can include headers. + - name: Make Linux headers visible to musl-gcc + if: runner.os == 'Linux' + run: | + set -eux + + # Get the GNU multiarch triplet for the current machine. + # Examples: + # x86_64-linux-gnu + # aarch64-linux-gnu + GNU_MULTIARCH="$(gcc -print-multiarch)" + + # Convert the GNU triplet into the musl include directory name. + # Examples: + # x86_64-linux-gnu -> x86_64-linux-musl + # aarch64-linux-gnu -> aarch64-linux-musl + MUSL_MULTIARCH="${GNU_MULTIARCH%-gnu}-musl" + + # musl-gcc searches this include directory. + MUSL_INCLUDE="/usr/include/${MUSL_MULTIARCH}" + + # Linux arch-specific asm headers are installed here by linux-libc-dev. + GNU_ASM="/usr/include/${GNU_MULTIARCH}/asm" + + # Ensure the musl include directory exists. + sudo mkdir -p "$MUSL_INCLUDE" + + # Remove old paths first. + # This avoids silently keeping broken/stale symlinks from previous runs. + sudo rm -rf "$MUSL_INCLUDE/linux" + sudo rm -rf "$MUSL_INCLUDE/asm" + sudo rm -rf "$MUSL_INCLUDE/asm-generic" + + # Expose Linux UAPI headers to musl-gcc. + # This fixes packages that include headers like . + sudo ln -s /usr/include/linux "$MUSL_INCLUDE/linux" + + # Expose architecture-specific asm headers to musl-gcc. + sudo ln -s "$GNU_ASM" "$MUSL_INCLUDE/asm" + + # Expose generic asm headers used by many Linux headers. + sudo ln -s /usr/include/asm-generic "$MUSL_INCLUDE/asm-generic" + + # Smoke test: fail early if musl-gcc still cannot find Linux headers. + echo '#include ' > /tmp/test.c + musl-gcc -c /tmp/test.c -o /tmp/test.o + - name: Use OCaml ${{matrix.ocaml_compiler}} uses: ocaml/setup-ocaml@v3.6.0 if: steps.cache-opam-env.outputs.cache-hit != 'true' diff --git a/dune b/dune index 91a5df6eca9..2903c721981 100644 --- a/dune +++ b/dune @@ -1 +1 @@ -(dirs compiler tests analysis tools) +(dirs compiler tests analysis tools lsp) diff --git a/dune-project b/dune-project index 112c97df1b6..71c1c5f0df0 100644 --- a/dune-project +++ b/dune-project @@ -75,3 +75,18 @@ (yojson (= 3.0.0)) (odoc :with-doc))) + +(package + (name rescript-language-server) + (synopsis "ReScript LSP") + (depends + (ocaml + (>= 4.10)) + (lsp + (>= 1.22.0)) + (eio + (>= 1.3)) + (eio_main + (>= 1.3)) + analysis + dune)) diff --git a/lsp/bin/dune b/lsp/bin/dune new file mode 100644 index 00000000000..ecd09b26ec7 --- /dev/null +++ b/lsp/bin/dune @@ -0,0 +1,5 @@ +(executable + (name main) + (package rescript-language-server) + (public_name rescript-language-server) + (libraries rescript_language_server)) diff --git a/lsp/bin/main.ml b/lsp/bin/main.ml new file mode 100644 index 00000000000..73ed8920da0 --- /dev/null +++ b/lsp/bin/main.ml @@ -0,0 +1 @@ +let () = Rescript_language_server.main () diff --git a/lsp/bin/main.mli b/lsp/bin/main.mli new file mode 100644 index 00000000000..e69de29bb2d diff --git a/lsp/src/configuration.ml b/lsp/src/configuration.ml new file mode 100644 index 00000000000..e69de29bb2d diff --git a/lsp/src/diagnostics.ml b/lsp/src/diagnostics.ml new file mode 100644 index 00000000000..de172eda24b --- /dev/null +++ b/lsp/src/diagnostics.ml @@ -0,0 +1,5 @@ +module UriMap = Map.Make (Lsp.Uri) + +type t = Lsp.Types.Diagnostic.t list UriMap.t + +let create () = UriMap.empty diff --git a/lsp/src/document_store.ml b/lsp/src/document_store.ml new file mode 100644 index 00000000000..d818d61f60d --- /dev/null +++ b/lsp/src/document_store.ml @@ -0,0 +1,30 @@ +(* module UriMap = Map.Make (Lsp.Uri) *) + +type document = {text: string; version: int} + +type t = {documents: (Lsp.Uri.t, document) Hashtbl.t} + +let create () = {documents = Hashtbl.create 25} + +let open_document t ~uri ~text ~version = + Hashtbl.add t.documents uri {text; version}; + t + +let update_document t ~uri ~text ~version = + (match Hashtbl.find_opt t.documents uri with + | None -> + raise + (Failure (Printf.sprintf "Document not found: %s" (Lsp.Uri.to_string uri))) + | Some _ -> Hashtbl.replace t.documents uri {text; version}); + t + +let remove_document t ~uri = + Hashtbl.remove t.documents uri; + t + +let get_document t ~uri = + match Hashtbl.find_opt t.documents uri with + | Some doc -> doc + | None -> + raise + (Failure (Printf.sprintf "Document not found: %s" (Lsp.Uri.to_string uri))) diff --git a/lsp/src/dune b/lsp/src/dune new file mode 100644 index 00000000000..486415966af --- /dev/null +++ b/lsp/src/dune @@ -0,0 +1,5 @@ +(library + (name rescript_language_server) + (libraries lsp eio eio_main analysis) + (flags + (-w "-9"))) diff --git a/lsp/src/hover.ml b/lsp/src/hover.ml new file mode 100644 index 00000000000..32eb4670fd7 --- /dev/null +++ b/lsp/src/hover.ml @@ -0,0 +1,16 @@ +open Lsp.Types + +let create ~(position : Position.t) ~(uri : DocumentUri.t) + (server : State.t Server.t) = + let path = DocumentUri.to_path uri in + let pos = (position.line, position.character) in + + (* NOTE: Should be a config *) + let supportsMarkdownLinks = true in + let debug = false in + let open Analysis in + let source = (Document_store.get_document ~uri server.state.store).text in + let kindFile = Files.classifySourceFile path in + let full = Cmt.loadFullCmtFromPath ~path in + + Commands.hover ~source ~kindFile ~pos ~debug ~supportsMarkdownLinks ~full diff --git a/lsp/src/rescript_language_server.ml b/lsp/src/rescript_language_server.ml new file mode 100644 index 00000000000..b6530ddc481 --- /dev/null +++ b/lsp/src/rescript_language_server.ml @@ -0,0 +1,79 @@ +let initialization (client_capabilities : Lsp.Types.ClientCapabilities.t) = + let open Lsp.Types in + let textDocumentSync = + `TextDocumentSyncOptions + (TextDocumentSyncOptions.create ~openClose:true + ~change:TextDocumentSyncKind.Full ~willSave:false + ~save:(`SaveOptions (SaveOptions.create ~includeText:false ())) + ~willSaveWaitUntil:false ()) + in + let capabilities = + ServerCapabilities.create ~textDocumentSync ~hoverProvider:(`Bool true) () + in + let serverInfo = + let version = "2.0.0-aplha.1" in + InitializeResult.create_serverInfo ~name:"rescript-language-server" ~version + () + in + InitializeResult.create ~capabilities ~serverInfo () + +let on_initialize (params : Lsp.Types.InitializeParams.t) (state : State.t) = + (* TODO: + * Find root project (rescript.json, package.json) using InitializeParams.workspaceFolders and save in State.t + * See https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#initializeParams + * If not found rescript.json kill the server? + * Save initializationOptions in State.t + * This options are: askToStartBuild, codeLens.enable, inlayHints.enable, etc.. + * Collect compiler diagnostics (syntax and type)? + *) + let diagnostics = Diagnostics.create () in + let initialization_info = initialization params.capabilities in + let state = State.initialize state ~params ~diagnostics in + (initialization_info, state) + +let on_request (Lsp.Client_request.E request) (server : State.t Server.t) = + let state = Server.state server in + let ok value = Ok (Lsp.Client_request.yojson_of_result request value) in + match request with + | Lsp.Client_request.Initialize params -> + let initialization_info, state = on_initialize params state in + (ok initialization_info, state) + | Shutdown -> (ok (), state) + | TextDocumentHover {position; textDocument = {uri}} -> + (ok (Hover.create ~position ~uri server), state) + | _ -> + let err = + Jsonrpc.Response.Error.make + ~code:Jsonrpc.Response.Error.Code.MethodNotFound + ~message:"Request method not supported" () + in + (Error err, state) + +let on_notification notification (server : State.t Server.t) = + let state = Server.state server in + + match notification with + | Lsp.Client_notification.TextDocumentDidOpen + {textDocument = {uri; text; version; _}} -> + let store = Document_store.open_document ~uri ~text ~version state.store in + {state with store} + (* | TextDocumentDidChange {textDocument = {uri; version; _}; contentChanges} + -> ( + match List.rev contentChanges with + | {text; _} :: _ -> state + | [] -> state) *) + | TextDocumentDidClose {textDocument = {uri; _}} -> + (* TODO: + * remove state diagnostics + * send updated diagnostics? + *) + let store = Document_store.remove_document ~uri state.store in + {state with store} + | Exit -> state + | _ -> state + +let main () = + Eio_main.run (fun env -> + let state = State.create ~store:(Document_store.create ()) in + Server.listen ~input:env#stdin ~output:env#stdout ~on_request + ~on_notification ~state ~env) diff --git a/lsp/src/server.ml b/lsp/src/server.ml new file mode 100644 index 00000000000..d2c4edc0977 --- /dev/null +++ b/lsp/src/server.ml @@ -0,0 +1,159 @@ +module Io : sig + type 'a t + + val return : 'a -> 'a t + val raise : exn -> 'a t + val await : 'a t -> 'a + val async : (sw:Eio.Switch.t -> ('a, exn) result) -> 'a t + + module O : sig + val ( let+ ) : 'a t -> ('a -> 'b) -> 'b t + val ( let* ) : 'a t -> ('a -> 'b t) -> 'b t + end +end = struct + type 'a t = sw:Eio.Switch.t -> ('a, exn) result Eio.Promise.t + + let await t = Eio.Switch.run @@ fun sw -> Eio.Promise.await_exn (t ~sw) + let return value ~sw:_ = Eio.Promise.create_resolved (Ok value) + let error desc ~sw:_ = Eio.Promise.create_resolved (Error desc) + + let async f ~sw = + let promise, resolver = Eio.Promise.create () in + ( Eio.Fiber.fork ~sw @@ fun () -> + try + let result = f ~sw in + Eio.Promise.resolve resolver result + with exn -> Eio.Promise.resolve resolver @@ Error exn ); + promise + + let bind t f = + async @@ fun ~sw -> + match Eio.Promise.await (t ~sw) with + | Ok value -> Eio.Promise.await @@ f value ~sw + | Error desc -> Error desc + + let raise = error + + module O = struct + let ( let+ ) x f = bind x @@ fun value -> return @@ f value + let ( let* ) = bind + end +end + +module Chan : sig + type input + type output + + val of_source : [> Eio__Flow.source_ty] Eio.Resource.t -> input + val with_sink : [> Eio__Flow.sink_ty] Eio.Resource.t -> (output -> 'a) -> 'a + + val read_line : input -> string option Io.t + val read_exactly : input -> int -> string option Io.t + val write : output -> string list -> unit Io.t +end = struct + type input = {mutex: Eio.Mutex.t; buf: Eio.Buf_read.t} + type output = {mutex: Eio.Mutex.t; buf: Eio.Buf_write.t} + + let initial_size = 1024 + let max_size = 1024 * 1024 + + let of_source source : input = + let mutex = Eio.Mutex.create () in + let buf = Eio.Buf_read.of_flow ~initial_size ~max_size source in + {mutex; buf} + + let with_sink sink f = + let mutex = Eio.Mutex.create () in + Eio.Buf_write.with_flow ~initial_size sink @@ fun buf -> f {mutex; buf} + + let read_line (input : input) = + Io.async @@ fun ~sw:_ -> + Eio.Mutex.use_rw ~protect:true input.mutex @@ fun () -> + if Eio.Buf_read.eof_seen input.buf then Ok None + else + match Eio.Buf_read.line input.buf with + | line -> Ok (Some line) + | exception End_of_file -> Ok None + + let read_exactly (input : input) size = + Io.async @@ fun ~sw:_ -> + Eio.Mutex.use_rw ~protect:true input.mutex @@ fun () -> + if Eio.Buf_read.eof_seen input.buf then Ok None + else + match Eio.Buf_read.take size input.buf with + | data -> Ok (Some data) + | exception End_of_file -> Ok None + + let write (output : output) (str : string list) = + Io.async @@ fun ~sw:_ -> + Eio.Mutex.use_rw ~protect:true output.mutex @@ fun () -> + Ok (List.iter (Eio.Buf_write.string output.buf) str) +end + +module Lsp_Io = Lsp.Io.Make (Io) (Chan) + +let notification_of_jsonrpc notification = + match Lsp.Client_notification.of_jsonrpc notification with + | Ok notification -> notification + | Error error -> raise (Lsp.Io.Error error) + +type 'a t = {channel: Chan.output; env: Eio_unix.Stdenv.base; state: 'a} + +let state t = t.state + +let respond server response = + Io.await @@ Lsp_Io.write server.channel @@ Response response + +let notification server notification = + let notification = Lsp.Server_notification.to_jsonrpc notification in + Io.await @@ Lsp_Io.write server.channel @@ Notification notification + +let log_message_notification ?(kind = Lsp.Types.MessageType.Debug) server + message = + notification server + (Lsp.Server_notification.LogMessage + (Lsp.Types.LogMessageParams.create ~type_:kind ~message)) + +let rec input_loop ~input ~state with_ = + match Io.await @@ Lsp_Io.read input with + | Some packet -> + let state = with_ state packet in + input_loop ~input ~state with_ + | exception exn -> raise (Failure "Server.input_loop") + | None -> () + +let listen ~input ~output ~on_request ~on_notification ~state ~env = + let handle_request server request = + let response, state = + match Lsp.Client_request.of_jsonrpc request with + | Error message -> + let code = Jsonrpc.Response.Error.Code.InvalidParams in + let err = Jsonrpc.Response.Error.make ~code ~message () in + (Jsonrpc.Response.{id = request.id; result = Error err}, state) + | Ok packed -> + let result, state = on_request packed server in + (Jsonrpc.Response.{id = request.id; result}, state) + in + respond server response; + state + in + let handle_notification server notification = + on_notification (notification_of_jsonrpc notification) server + in + let input = Chan.of_source input in + Chan.with_sink output (fun channel -> + let server = {channel; state; env} in + input_loop ~input ~state (fun state packet -> + match packet with + | Notification notification -> handle_notification server notification + | Request request -> handle_request server request + | Batch_call calls -> + List.fold_left + (fun state call -> + match call with + | `Request request -> handle_request server request + | `Notification notification -> + handle_notification server notification) + state calls + | Response _ -> raise (Lsp.Io.Error "unexpected response") + | Batch_response _ -> raise (Lsp.Io.Error "unexpected batch response"))) diff --git a/lsp/src/state.ml b/lsp/src/state.ml new file mode 100644 index 00000000000..e5e87932119 --- /dev/null +++ b/lsp/src/state.ml @@ -0,0 +1,13 @@ +open Lsp.Types + +type status = + | Uninitialized + | Initialized of {params: InitializeParams.t; diagnostics: Diagnostics.t} + +(* TODO: add trace, configuration *) +type t = {status: status; store: Document_store.t} + +let create ~store = {status = Uninitialized; store} + +let initialize t ~params ~diagnostics = + {t with status = Initialized {params; diagnostics}} diff --git a/package.json b/package.json index 88b253b0eca..81df1c032fc 100644 --- a/package.json +++ b/package.json @@ -107,6 +107,7 @@ "tests/tests", "tests/tools_tests", "tests/commonjs_tests", + "tests/lsp_tests/**", "scripts/res" ], "packageManager": "yarn@4.12.0", diff --git a/rescript-language-server.opam b/rescript-language-server.opam new file mode 100644 index 00000000000..6b6aa9366a9 --- /dev/null +++ b/rescript-language-server.opam @@ -0,0 +1,31 @@ +# This file is generated by dune, edit dune-project instead +opam-version: "2.0" +synopsis: "ReScript LSP" +maintainer: ["Hongbo Zhang " "Cristiano Calcagno"] +authors: ["Hongbo Zhang "] +license: "LGPL-3.0-or-later" +homepage: "https://github.com/rescript-lang/rescript-compiler" +bug-reports: "https://github.com/rescript-lang/rescript-compiler/issues" +depends: [ + "ocaml" {>= "4.10"} + "lsp" {>= "1.22.0"} + "eio" {>= "1.3"} + "eio_main" {>= "1.3"} + "analysis" + "dune" {>= "3.17"} + "odoc" {with-doc} +] +build: [ + ["dune" "subst"] {dev} + [ + "dune" + "build" + "-p" + name + "-j" + jobs + "@install" + "@runtest" {with-test} + "@doc" {with-doc} + ] +] diff --git a/tests/dune b/tests/dune index 01dd377b945..d9dd6567304 100644 --- a/tests/dune +++ b/tests/dune @@ -1 +1 @@ -(dirs ounit_tests syntax_benchmarks syntax_tests) +(dirs ounit_tests syntax_benchmarks syntax_tests lsp_tests) diff --git a/tests/lsp_tests/basic-workspace/Hover.res b/tests/lsp_tests/basic-workspace/Hover.res new file mode 100644 index 00000000000..230cdafee6e --- /dev/null +++ b/tests/lsp_tests/basic-workspace/Hover.res @@ -0,0 +1,286 @@ +let abc = 22 + 34 +// ^hov + +type t = (int, float) +// ^hov + +module Id = { + // ^hov + type x = int +} + +@ocaml.doc("This module is commented") +module Dep: { + @ocaml.doc("Some doc comment") + let customDouble: int => int +} = { + let customDouble = foo => foo * 2 +} + +module D = Dep +// ^hov + +let cd = D.customDouble +// ^hov + +module HoverInsideModuleWithComponent = { + let x = 2 // check that hover on x works + // ^hov + @react.component + let make = () => React.null +} + +@ocaml.doc("Doc comment for functionWithTypeAnnotation") +let functionWithTypeAnnotation: unit => int = () => 1 +// ^hov + +@react.component +let make = (~name) => React.string(name) +// ^hov + +module C2 = { + @react.component + let make2 = (~name: string) => React.string(name) + // ^hov +} + +let num = 34 +// ^hov + +module type Logger = { + // ^hov + let log: string => unit +} + +module JsLogger: Logger = { + // ^hov + let log = (msg: string) => Console.log(msg) + let _oneMore = 3 +} + +module JJ = JsLogger +// ^def + +module IdDefinedTwice = { + // ^hov + let _x = 10 + let y = 20 + let _x = 10 +} + +module A = { + let x = 13 +} + +module B = A +// ^hov + +module C = B +// ^hov + +module Comp = { + @react.component + let make = (~children: React.element) => children +} + +module Comp1 = Comp + +let _ = + +
+
+ +// ^hov + +let _ = + +
+
+ +// ^hov + +type r<'a> = {i: 'a, f: float} + +let _get = r => r.f +. r.i +// ^hov + +let withAs = (~xx as yyy) => yyy + 1 +// ^hov + +module AA = { + type cond<'a> = [< #str(string)] as 'a + let fnnxx = (b: cond<_>) => true ? b : b +} + +let funAlias = AA.fnnxx + +let typeOk = funAlias +// ^hov + +let typeDuplicate = AA.fnnxx +// ^hov + +@live let dd = 34 +// ^hov + +let arity0a = () => { + //^hov + let f = () => 3 + f +} + +let arity0b = ((), ()) => 3 +// ^hov + +let arity0c = ((), ()) => 3 +// ^hov + +let arity0d = () => { + // ^hov + let f = () => 3 + f +} + +/**doc comment 1*/ +let docComment1 = 12 +// ^hov + +/** doc comment 2 */ +let docComment2 = 12 +// ^hov + +module ModWithDocComment = { + /*** module level doc comment 1 */ + + /** doc comment for x */ + let x = 44 + + /*** module level doc comment 2 */ +} + +module TypeSubstitutionRecords = { + type foo<'a> = {content: 'a, zzz: string} + type bar = {age: int} + type foobar = foo + + let x1: foo = {content: {age: 42}, zzz: ""} + // ^hov + let x2: foobar = {content: {age: 42}, zzz: ""} + // ^hov + + // x1.content. + // ^com + + // x2.content. + // ^com + + type foo2<'b> = foo<'b> + type foobar2 = foo2 + + let y1: foo2 = {content: {age: 42}, zzz: ""} + let y2: foobar2 = {content: {age: 42}, zzz: ""} + + // y1.content. + // ^com + + // y2.content. + // ^com +} + +module CompV4 = { + type props<'n, 's> = {n?: 'n, s: 's} + let make = props => { + let _ = props.n == Some(10) + React.string(props.s) + } +} + +let mk = CompV4.make +// ^hov + +type useR = {x: int, y: list>>} + +let testUseR = (v: useR) => v +// ^hov + +let usr: useR = { + x: 123, + y: list{}, +} + +// let f = usr +// ^hov + +module NotShadowed = { + /** Stuff */ + let xx_ = 10 + + /** More Stuff */ + let xx = xx_ +} + +module Shadowed = { + /** Stuff */ + let xx = 10 + + /** More Stuff */ + let xx = xx +} + +let _ = NotShadowed.xx +// ^hov + +let _ = Shadowed.xx +// ^hov + +type recordWithDocstringField = { + /** Mighty fine field here. */ + someField: bool, +} + +let x: recordWithDocstringField = { + someField: true, +} + +// x.someField +// ^hov + +let someField = x.someField +// ^hov + +type variant = + /** Cool variant! */ + | CoolVariant + /** Other cool variant */ + | OtherCoolVariant + +let coolVariant = CoolVariant +// ^hov + +type payloadVariants = InlineRecord({field1: int, field2: bool}) | Args(int, bool) + +let payloadVariant = InlineRecord({field1: 1, field2: true}) +// ^hov + +let payloadVariant2 = Args(1, true) +// ^hov + +module RecursiveVariants = { + type rec t = Action1(int) | Action2(float) | Batch(array) +} + +let recursiveVariant = RecursiveVariants.Action1(1) +// ^hov + +// Hover on unsaved +// let fff = "hello"; fff +// ^hov + +// switch x { | {someField} => someField } +// ^hov + +module Arr = Belt.Array +// ^hov + +type aliased = variant +// ^hov diff --git a/tests/lsp_tests/basic-workspace/Hover.res.expected b/tests/lsp_tests/basic-workspace/Hover.res.expected new file mode 100644 index 00000000000..e5a6f734da4 --- /dev/null +++ b/tests/lsp_tests/basic-workspace/Hover.res.expected @@ -0,0 +1,341 @@ +Request textDocument/hover tests/lsp_tests/basic-workspace/Hover.res:1:5 +Response +{ "contents": { "kind": "markdown", "value": "```rescript\nint\n```" } } + +Request textDocument/hover tests/lsp_tests/basic-workspace/Hover.res:4:6 +Response +{ + "contents": { + "kind": "markdown", + "value": "```rescript\ntype t = (int, float)\n```" + } +} + +Request textDocument/hover tests/lsp_tests/basic-workspace/Hover.res:7:8 +Response +{ + "contents": { + "kind": "markdown", + "value": "```rescript\nmodule Id: {\n type x = int\n}\n```" + } +} + +Request textDocument/hover tests/lsp_tests/basic-workspace/Hover.res:20:12 +Response +{ + "contents": { + "kind": "markdown", + "value": "\nThis module is commented\n---\n\n```\n \n```\n```rescript\nmodule Dep: {\n let customDouble: int => int\n}\n```" + } +} + +Request textDocument/hover tests/lsp_tests/basic-workspace/Hover.res:23:12 +Response +{ + "contents": { + "kind": "markdown", + "value": "```rescript\nint => int\n```\n---\nSome doc comment" + } +} + +Request textDocument/hover tests/lsp_tests/basic-workspace/Hover.res:27:7 +Response +{ "contents": { "kind": "markdown", "value": "```rescript\nint\n```" } } + +Request textDocument/hover tests/lsp_tests/basic-workspace/Hover.res:34:5 +Response +{ + "contents": { + "kind": "markdown", + "value": "```rescript\nunit => int\n```\n---\nDoc comment for functionWithTypeAnnotation" + } +} + +Request textDocument/hover tests/lsp_tests/basic-workspace/Hover.res:38:14 +Response +{ "contents": { "kind": "markdown", "value": "```rescript\nstring\n```" } } + +Request textDocument/hover tests/lsp_tests/basic-workspace/Hover.res:43:16 +Response +{ "contents": { "kind": "markdown", "value": "```rescript\nstring\n```" } } + +Request textDocument/hover tests/lsp_tests/basic-workspace/Hover.res:47:11 +Response +{ "contents": { "kind": "markdown", "value": "```rescript\nint\n```" } } + +Request textDocument/hover tests/lsp_tests/basic-workspace/Hover.res:50:14 +Response +{ + "contents": { + "kind": "markdown", + "value": "```rescript\nmodule type Logger = {\n let log: string => unit\n}\n```" + } +} + +Request textDocument/hover tests/lsp_tests/basic-workspace/Hover.res:55:8 +Response +{ + "contents": { + "kind": "markdown", + "value": "```rescript\nmodule type Logger = {\n let log: string => unit\n}\n```" + } +} + +Command `def` not implemented! tests/lsp_tests/basic-workspace/Hover.res:61:15 + +Request textDocument/hover tests/lsp_tests/basic-workspace/Hover.res:64:10 +Response +{ + "contents": { + "kind": "markdown", + "value": "```rescript\nmodule IdDefinedTwice: {\n let y: int\n let _x: int\n}\n```" + } +} + +Request textDocument/hover tests/lsp_tests/basic-workspace/Hover.res:75:8 +Response +{ + "contents": { + "kind": "markdown", + "value": "```rescript\nmodule A: {\n let x: int\n}\n```" + } +} + +Request textDocument/hover tests/lsp_tests/basic-workspace/Hover.res:78:8 +Response +{ + "contents": { + "kind": "markdown", + "value": "```rescript\nmodule A: {\n let x: int\n}\n```" + } +} + +Request textDocument/hover tests/lsp_tests/basic-workspace/Hover.res:92:11 +Response +null + +Request textDocument/hover tests/lsp_tests/basic-workspace/Hover.res:99:11 +Response +null + +Request textDocument/hover tests/lsp_tests/basic-workspace/Hover.res:104:26 +Response +{ "contents": { "kind": "markdown", "value": "```rescript\nfloat\n```" } } + +Request textDocument/hover tests/lsp_tests/basic-workspace/Hover.res:107:22 +Response +{ "contents": { "kind": "markdown", "value": "```rescript\nint\n```" } } + +Request textDocument/hover tests/lsp_tests/basic-workspace/Hover.res:117:17 +Response +{ + "contents": { + "kind": "markdown", + "value": "```rescript\nAA.cond<([< #str(string)] as 'a)> => AA.cond<'a>\n```\n\n---\n\n```\n \n```\n```rescript\ntype AA.cond<'a> = 'a\n constraint 'a = [< #str(string)]\n```\nGo to: [Type definition](command:rescript-vscode.go_to_location?%5B%22file%3A%2F%2F%2Fhome%2Fpedro%2FDesktop%2Fprojects%2Frescript-compiler%2Ftests%2Flsp_tests%2Fbasic-workspace%2FHover.res%22%2C110%2C2%5D)\n" + } +} + +Request textDocument/hover tests/lsp_tests/basic-workspace/Hover.res:120:26 +Response +{ + "contents": { + "kind": "markdown", + "value": "```rescript\nAA.cond<([< #str(string)] as 'a)> => AA.cond<'a>\n```\n\n---\n\n```\n \n```\n```rescript\ntype AA.cond<'a> = 'a\n constraint 'a = [< #str(string)]\n```\nGo to: [Type definition](command:rescript-vscode.go_to_location?%5B%22file%3A%2F%2F%2Fhome%2Fpedro%2FDesktop%2Fprojects%2Frescript-compiler%2Ftests%2Flsp_tests%2Fbasic-workspace%2FHover.res%22%2C110%2C2%5D)\n" + } +} + +Request textDocument/hover tests/lsp_tests/basic-workspace/Hover.res:123:4 +Response +{ + "contents": { + "kind": "markdown", + "value": "The `@live` decorator is for reanalyze, a static analysis tool for ReScript that can do dead code analysis.\n\n`@live` tells the dead code analysis that the value should be considered live, even though it might appear to be dead. This is typically used in case of FFI where there are indirect ways to access values. It can be added to everything that could otherwise be considered unused by the dead code analysis - values, functions, arguments, records, individual record fields, and so on.\n\n[Read more and see examples in the documentation](https://rescript-lang.org/syntax-lookup#live-decorator).\n\nHint: Did you know you can run an interactive code analysis in your project by running the command `> ReScript: Start Code Analyzer`? Try it!" + } +} + +Request textDocument/hover tests/lsp_tests/basic-workspace/Hover.res:132:5 +Response +{ + "contents": { + "kind": "markdown", + "value": "```rescript\n(unit, unit) => int\n```" + } +} + +Request textDocument/hover tests/lsp_tests/basic-workspace/Hover.res:135:5 +Response +{ + "contents": { + "kind": "markdown", + "value": "```rescript\n(unit, unit) => int\n```" + } +} + +Request textDocument/hover tests/lsp_tests/basic-workspace/Hover.res:138:6 +Response +{ + "contents": { + "kind": "markdown", + "value": "```rescript\nunit => unit => int\n```" + } +} + +Request textDocument/hover tests/lsp_tests/basic-workspace/Hover.res:145:10 +Response +{ + "contents": { + "kind": "markdown", + "value": "```rescript\nint\n```\n---\ndoc comment 1" + } +} + +Request textDocument/hover tests/lsp_tests/basic-workspace/Hover.res:149:7 +Response +{ + "contents": { + "kind": "markdown", + "value": "```rescript\nint\n```\n---\n doc comment 2 " + } +} + +Request textDocument/hover tests/lsp_tests/basic-workspace/Hover.res:166:24 +Response +{ + "contents": { + "kind": "markdown", + "value": "```rescript\nfoo\n```\n\n---\n\n```\n \n```\n```rescript\ntype foo<'a> = {content: 'a, zzz: string}\n```\nGo to: [Type definition](command:rescript-vscode.go_to_location?%5B%22file%3A%2F%2F%2Fhome%2Fpedro%2FDesktop%2Fprojects%2Frescript-compiler%2Ftests%2Flsp_tests%2Fbasic-workspace%2FHover.res%22%2C161%2C2%5D)\n\n\n---\n\n```\n \n```\n```rescript\ntype bar = {age: int}\n```\nGo to: [Type definition](command:rescript-vscode.go_to_location?%5B%22file%3A%2F%2F%2Fhome%2Fpedro%2FDesktop%2Fprojects%2Frescript-compiler%2Ftests%2Flsp_tests%2Fbasic-workspace%2FHover.res%22%2C162%2C2%5D)\n" + } +} + +Request textDocument/hover tests/lsp_tests/basic-workspace/Hover.res:168:23 +Response +{ + "contents": { + "kind": "markdown", + "value": "```rescript\nfoobar\n```\n\n---\n\n```\n \n```\n```rescript\ntype foobar = foo\n```\nGo to: [Type definition](command:rescript-vscode.go_to_location?%5B%22file%3A%2F%2F%2Fhome%2Fpedro%2FDesktop%2Fprojects%2Frescript-compiler%2Ftests%2Flsp_tests%2Fbasic-workspace%2FHover.res%22%2C163%2C2%5D)\n" + } +} + +Command `com` not implemented! tests/lsp_tests/basic-workspace/Hover.res:171:17 + +Command `com` not implemented! tests/lsp_tests/basic-workspace/Hover.res:174:17 + +Command `com` not implemented! tests/lsp_tests/basic-workspace/Hover.res:183:17 + +Command `com` not implemented! tests/lsp_tests/basic-workspace/Hover.res:186:17 + +Request textDocument/hover tests/lsp_tests/basic-workspace/Hover.res:198:5 +Response +{ + "contents": { + "kind": "markdown", + "value": "```rescript\nCompV4.props => React.element\n```\n\n---\n\n```\n \n```\n```rescript\ntype CompV4.props<'n, 's> = {n?: 'n, s: 's}\n```\nGo to: [Type definition](command:rescript-vscode.go_to_location?%5B%22file%3A%2F%2F%2Fhome%2Fpedro%2FDesktop%2Fprojects%2Frescript-compiler%2Ftests%2Flsp_tests%2Fbasic-workspace%2FHover.res%22%2C190%2C2%5D)\n\n\n---\n\n```\n \n```\n```rescript\ntype React.element = Jsx.element\n```\nGo to: [Type definition](command:rescript-vscode.go_to_location?%5B%22file%3A%2F%2F%2Fhome%2Fpedro%2FDesktop%2Fprojects%2Frescript-compiler%2Ftests%2Fdependencies%2Frescript-react%2Fsrc%2FReact.res%22%2C0%2C0%5D)\n" + } +} + +Request textDocument/hover tests/lsp_tests/basic-workspace/Hover.res:203:17 +Response +{ + "contents": { + "kind": "markdown", + "value": "```rescript\nuseR\n```\n\n---\n\n```\n \n```\n```rescript\ntype useR = {x: int, y: list>>}\n```\nGo to: [Type definition](command:rescript-vscode.go_to_location?%5B%22file%3A%2F%2F%2Fhome%2Fpedro%2FDesktop%2Fprojects%2Frescript-compiler%2Ftests%2Flsp_tests%2Fbasic-workspace%2FHover.res%22%2C200%2C0%5D)\n\n\n---\n\n```\n \n```\n```rescript\ntype r<'a> = {i: 'a, f: float}\n```\nGo to: [Type definition](command:rescript-vscode.go_to_location?%5B%22file%3A%2F%2F%2Fhome%2Fpedro%2FDesktop%2Fprojects%2Frescript-compiler%2Ftests%2Flsp_tests%2Fbasic-workspace%2FHover.res%22%2C101%2C0%5D)\n" + } +} + +Request textDocument/hover tests/lsp_tests/basic-workspace/Hover.res:211:14 +Response +null + +Request textDocument/hover tests/lsp_tests/basic-workspace/Hover.res:230:21 +Response +{ + "contents": { + "kind": "markdown", + "value": "```rescript\nint\n```\n---\n More Stuff " + } +} + +Request textDocument/hover tests/lsp_tests/basic-workspace/Hover.res:233:18 +Response +{ + "contents": { + "kind": "markdown", + "value": "```rescript\nint\n```\n---\n More Stuff " + } +} + +Request textDocument/hover tests/lsp_tests/basic-workspace/Hover.res:245:7 +Response +null + +Request textDocument/hover tests/lsp_tests/basic-workspace/Hover.res:248:20 +Response +{ + "contents": { + "kind": "markdown", + "value": "```rescript\nbool\n```\n---\n Mighty fine field here. " + } +} + +Request textDocument/hover tests/lsp_tests/basic-workspace/Hover.res:257:21 +Response +{ + "contents": { + "kind": "markdown", + "value": "```rescript\nvariant\nCoolVariant\n```\n---\n Cool variant! \n\n---\n\n```\n \n```\n```rescript\ntype variant = CoolVariant | OtherCoolVariant\n```\nGo to: [Type definition](command:rescript-vscode.go_to_location?%5B%22file%3A%2F%2F%2Fhome%2Fpedro%2FDesktop%2Fprojects%2Frescript-compiler%2Ftests%2Flsp_tests%2Fbasic-workspace%2FHover.res%22%2C250%2C0%5D)\n" + } +} + +Request textDocument/hover tests/lsp_tests/basic-workspace/Hover.res:262:23 +Response +{ + "contents": { + "kind": "markdown", + "value": "```rescript\npayloadVariants\nInlineRecord({field1: int, field2: bool})\n```\n\n---\n\n```\n \n```\n```rescript\ntype payloadVariants =\n | InlineRecord({field1: int, field2: bool})\n | Args(int, bool)\n```\nGo to: [Type definition](command:rescript-vscode.go_to_location?%5B%22file%3A%2F%2F%2Fhome%2Fpedro%2FDesktop%2Fprojects%2Frescript-compiler%2Ftests%2Flsp_tests%2Fbasic-workspace%2FHover.res%22%2C259%2C0%5D)\n" + } +} + +Request textDocument/hover tests/lsp_tests/basic-workspace/Hover.res:265:24 +Response +{ + "contents": { + "kind": "markdown", + "value": "```rescript\npayloadVariants\nArgs(int, bool)\n```\n\n---\n\n```\n \n```\n```rescript\ntype payloadVariants =\n | InlineRecord({field1: int, field2: bool})\n | Args(int, bool)\n```\nGo to: [Type definition](command:rescript-vscode.go_to_location?%5B%22file%3A%2F%2F%2Fhome%2Fpedro%2FDesktop%2Fprojects%2Frescript-compiler%2Ftests%2Flsp_tests%2Fbasic-workspace%2FHover.res%22%2C259%2C0%5D)\n" + } +} + +Request textDocument/hover tests/lsp_tests/basic-workspace/Hover.res:272:43 +Response +{ + "contents": { + "kind": "markdown", + "value": "```rescript\nRecursiveVariants.t\nAction1(int)\n```\n\n---\n\n```\n \n```\n```rescript\ntype RecursiveVariants.t =\n | Action1(int)\n | Action2(float)\n | Batch(array)\n```\nGo to: [Type definition](command:rescript-vscode.go_to_location?%5B%22file%3A%2F%2F%2Fhome%2Fpedro%2FDesktop%2Fprojects%2Frescript-compiler%2Ftests%2Flsp_tests%2Fbasic-workspace%2FHover.res%22%2C268%2C2%5D)\n" + } +} + +Request textDocument/hover tests/lsp_tests/basic-workspace/Hover.res:276:24 +Response +null + +Request textDocument/hover tests/lsp_tests/basic-workspace/Hover.res:279:34 +Response +null + +Request textDocument/hover tests/lsp_tests/basic-workspace/Hover.res:282:9 +Response +{ + "contents": { + "kind": "markdown", + "value": "\n [`Belt.Array`]()\n\n **mutable array**: Utilities functions\n\n---\n\n```\n \n```\n```rescript\nmodule Array: {\n module Id\n module Array\n module SortArray\n module MutableQueue\n module MutableStack\n module List\n module Range\n module Set\n module Map\n module MutableSet\n module MutableMap\n module HashSet\n module HashMap\n module Option\n module Result\n module Int\n module Float\n}\n```" + } +} + +Request textDocument/hover tests/lsp_tests/basic-workspace/Hover.res:285:7 +Response +{ + "contents": { + "kind": "markdown", + "value": "```rescript\ntype aliased = variant\n```\n\n---\n\n```\n \n```\n```rescript\ntype variant = CoolVariant | OtherCoolVariant\n```\nGo to: [Type definition](command:rescript-vscode.go_to_location?%5B%22file%3A%2F%2F%2Fhome%2Fpedro%2FDesktop%2Fprojects%2Frescript-compiler%2Ftests%2Flsp_tests%2Fbasic-workspace%2FHover.res%22%2C250%2C0%5D)\n" + } +} + diff --git a/tests/lsp_tests/basic-workspace/package.json b/tests/lsp_tests/basic-workspace/package.json new file mode 100644 index 00000000000..950bea0a1f9 --- /dev/null +++ b/tests/lsp_tests/basic-workspace/package.json @@ -0,0 +1,14 @@ +{ + "name": "@tests/lsp-tests-basic-workspace", + "type": "module", + "private": true, + "scripts": { + "build": "rescript build", + "clean": "rescript clean", + "dev": "rescript -w" + }, + "dependencies": { + "@rescript/react": "workspace:^", + "rescript": "workspace:^" + } +} diff --git a/tests/lsp_tests/basic-workspace/rescript.json b/tests/lsp_tests/basic-workspace/rescript.json new file mode 100644 index 00000000000..0f6ec0f0d92 --- /dev/null +++ b/tests/lsp_tests/basic-workspace/rescript.json @@ -0,0 +1,13 @@ +{ + "name": "@tests/lsp-tests-basic-workspace", + "sources": { + "dir": "." + }, + "package-specs": { + "module": "esmodule", + "in-source": false + }, + "suffix": ".res.js", + "dependencies": ["@rescript/react"], + "jsx": { "version": 4 } +} diff --git a/tests/lsp_tests/dune b/tests/lsp_tests/dune new file mode 100644 index 00000000000..fd9baef10d0 --- /dev/null +++ b/tests/lsp_tests/dune @@ -0,0 +1,10 @@ +(executable + (name test) + (package rescript-language-server) + (public_name lsp-tests) + (libraries lsp jsonrpc yojson eio eio_main eio.unix) + (flags + (-w "-9-32-33"))) + +(dirs + (:standard \ ignored_dir basic-workspace)) diff --git a/tests/lsp_tests/test.ml b/tests/lsp_tests/test.ml new file mode 100644 index 00000000000..62319d87d7e --- /dev/null +++ b/tests/lsp_tests/test.ml @@ -0,0 +1,275 @@ +let ( // ) = Filename.concat +let executable = "_build" // "default" // "lsp" // "bin" // "main.exe" + +module Client = struct + (** Helpers for spawning the ReScript language server in tests, sending + LSP requests/notifications over stdio, and reading responses back. *) + + type t = { + proc: [`Generic | `Unix] Eio.Process.ty Eio.Resource.t; + stdin: Eio_unix.sink_ty Eio.Resource.t; + stdout: Eio.Buf_read.t; + mutable next_id: int; + } + + let frame (json : Yojson.Safe.t) : string = + let body = Yojson.Safe.to_string json in + Printf.sprintf "Content-Length: %d\r\n\r\n%s" (String.length body) body + + let read_headers buf = + let rec loop acc = + match Eio.Buf_read.line buf with + | "" -> Some acc + | line -> + let acc = + match String.index_opt line ':' with + | None -> acc + | Some i -> + let k = String.sub line 0 i in + let v = + String.trim (String.sub line (i + 1) (String.length line - i - 1)) + in + (k, v) :: acc + in + loop acc + | exception End_of_file -> if acc = [] then None else Some acc + in + loop [] + + let read_message buf = + match read_headers buf with + | None -> None + | Some headers -> + let len = int_of_string (List.assoc "Content-Length" headers) in + let body = Eio.Buf_read.take len buf in + Some (Yojson.Safe.from_string body) + + let start ~sw ~env = + let mgr = Eio.Stdenv.process_mgr env in + let stdin_r, stdin_w = Eio_unix.pipe sw in + let stdout_r, stdout_w = Eio_unix.pipe sw in + let proc = + Eio.Process.spawn ~sw mgr ~stdin:stdin_r ~stdout:stdout_w ~executable [] + in + Eio.Resource.close stdin_r; + Eio.Resource.close stdout_w; + let stdout = Eio.Buf_read.of_flow ~max_size:(16 * 1024 * 1024) stdout_r in + {proc; stdin = stdin_w; stdout; next_id = 0} + + let send_packet t (packet : Jsonrpc.Packet.t) = + let json = Jsonrpc.Packet.yojson_of_t packet in + Eio.Flow.copy_string (frame json) t.stdin + + let next_id t = + t.next_id <- t.next_id + 1; + t.next_id + + (** Send a typed LSP request and return the assigned id. *) + let send_request t (req : 'r Lsp.Client_request.t) = + let id = `Int (next_id t) in + let jsonrpc_req = Lsp.Client_request.to_jsonrpc_request req ~id in + send_packet t (Jsonrpc.Packet.Request jsonrpc_req); + id + + (** Send a typed LSP notification. *) + let send_notification t (notif : Lsp.Client_notification.t) = + let jsonrpc_notif = Lsp.Client_notification.to_jsonrpc notif in + send_packet t (Jsonrpc.Packet.Notification jsonrpc_notif) + + (** Read packets until we find the response matching [id]. Server + notifications/requests received in the meantime are discarded. *) + let rec read_response t id = + match read_message t.stdout with + | None -> failwith "Helper.read_response: unexpected EOF" + | Some json -> ( + match Jsonrpc.Packet.t_of_yojson json with + | Response resp when resp.id = id -> resp + | _ -> read_response t id) + + (** Send a typed request and synchronously wait for its response, decoded + back into the request's result type. *) + let request (type r) t (req : r Lsp.Client_request.t) : r = + let id = send_request t req in + let resp = read_response t id in + match resp.result with + | Ok json -> Lsp.Client_request.response_of_json req json + | Error err -> failwith ("LSP error response: " ^ err.message) + + (** Read the next packet of any kind. Useful when waiting for a server + notification (e.g. publishDiagnostics). *) + (* let read_packet t = + match read_message t.stdout with + | None -> failwith "Helper.read_packet: unexpected EOF" + | Some json -> Jsonrpc.Packet.t_of_yojson json *) + + let stop t = + (try Eio.Resource.close t.stdin with _ -> ()); + Eio.Process.await t.proc + + (** Run [f] with a freshly started server, ensuring the process is stopped + and the switch is released afterwards. *) + let with_server ~env f = + Eio.Switch.run @@ fun sw -> + let t = start ~sw ~env in + Fun.protect ~finally:(fun () -> ignore (stop t)) (fun () -> f t) +end + +open Lsp +open Types + +type caret_comment = { + path: string; (* absolute path *) + line: int; (* line of the comment *) + col: int; (* column of the ^ character *) + command: string; (* e.g. "hov" *) + text: string; (* file content *) +} + +module String_map = Map.Make (String) + +let find_caret_comments ~fs ~workspace_dir = + let results = ref [] in + + (* Read all .res files in directory *) + Eio.Path.with_open_dir + Eio.Path.(fs / workspace_dir) + (fun dir_handle -> + Eio.Path.read_dir dir_handle + |> List.filter (fun file -> + String.ends_with ~suffix:".res" file + || String.ends_with ~suffix:".resi" file) + |> List.iter (fun filename -> + let path = Eio.Path.(dir_handle / filename) in + let content = Eio.Path.load path in + let lines = String.split_on_char '\n' content in + + List.iteri + (fun line_idx line -> + (* Match lines like "// ^command" *) + match String.trim line with + | s when String.length s > 3 && String.sub s 0 3 = "// " -> ( + let rest = String.sub s 3 (String.length s - 3) in + match String.index_opt rest '^' with + | None -> () + | Some caret_in_rest -> + (* Column of ^ in original line *) + let prefix_len = + String.length line - String.length (String.trim line) + in + let col = prefix_len + 3 + caret_in_rest in + let command = + let after = caret_in_rest + 1 in + if after < String.length rest then + String.trim + (String.sub rest after (String.length rest - after)) + else "" + in + results := + { + path = workspace_dir // snd path; + line = line_idx; + col; + command; + text = content; + } + :: !results) + | _ -> ()) + lines)); + + List.rev !results + +let open_document ~uri ~text client = + Client.send_notification client + (Client_notification.TextDocumentDidOpen + (DidOpenTextDocumentParams.create + ~textDocument: + (TextDocumentItem.create ~uri ~languageId:"rescript" ~version:0 + ~text))) + +let pretty_source_loc caret_comment = + let relative_path = + let dir_len = String.length (Sys.getcwd () ^ "/") in + String.sub caret_comment.path dir_len + (String.length caret_comment.path - dir_len) + in + + Printf.sprintf "%s:%d:%d" relative_path caret_comment.line + (caret_comment.col + 1) + +let send_request payload client = + let response = Client.request client payload in + Client_request.yojson_of_result payload response + |> Yojson.Safe.pretty_to_string ~std:true + +let print_response method_ response caret_comment = + Printf.sprintf "Request %s %s\nResponse\n%s\n\n" method_ + (pretty_source_loc caret_comment) + response + +let run_test_for_comment (caret_comment : caret_comment) client = + let uri = DocumentUri.of_path caret_comment.path in + let textDocument = TextDocumentIdentifier.create ~uri in + + let character = caret_comment.col in + let line = caret_comment.line - 1 in + let position = Position.create ~line ~character in + let text = caret_comment.text in + + match caret_comment.command with + | "hov" -> + open_document ~uri ~text client; + let resp = + send_request + (Client_request.TextDocumentHover + (HoverParams.create ~textDocument ~position ())) + client + in + print_response "textDocument/hover" resp caret_comment + (* | "cmp" -> + let context = + CompletionContext.create ~triggerCharacter:">" + ~triggerKind:CompletionTriggerKind.TriggerCharacter () + in + send_request + (Client_request.TextDocumentCompletion + (CompletionParams.create ~textDocument ~position ~context ())) + "textDocument/completion" caret_comment *) + | other -> + Printf.sprintf "Command `%s` not implemented! %s\n\n" other + (pretty_source_loc caret_comment) + +let run_workspace_test ~fs ~workspace_dir client = + let comments = find_caret_comments ~fs ~workspace_dir in + + let grouped = + List.fold_left + (fun acc comment -> + let others = + Option.value ~default:[] (String_map.find_opt comment.path acc) + in + String_map.add comment.path (comment :: others) acc) + String_map.empty comments + in + + String_map.iter + (fun path comments -> + let filename = Filename.basename path ^ ".expected" in + let save_path = workspace_dir // filename in + let content = + List.rev_map (fun c -> run_test_for_comment c client) comments + |> String.concat "" + in + let file = Eio.Path.(fs / save_path) in + Eio.Path.save ~create:(`Or_truncate 0o644) file content) + grouped + +let main () = + Eio_main.run @@ fun env -> + Client.with_server ~env @@ fun client -> + let workspace_dir = + Sys.getcwd () // "tests" // "lsp_tests" // "basic-workspace" + in + run_workspace_test ~fs:env#fs ~workspace_dir client; + Client.stop client |> ignore + +let () = main () diff --git a/yarn.lock b/yarn.lock index 1eeb1b12d84..4caa169c594 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1133,6 +1133,15 @@ __metadata: languageName: unknown linkType: soft +"@tests/lsp-tests-basic-workspace@workspace:tests/lsp_tests/basic-workspace": + version: 0.0.0-use.local + resolution: "@tests/lsp-tests-basic-workspace@workspace:tests/lsp_tests/basic-workspace" + dependencies: + "@rescript/react": "workspace:^" + rescript: "workspace:^" + languageName: unknown + linkType: soft + "@tests/namespaced-references@workspace:tests/analysis_tests/tests-namespaced-references": version: 0.0.0-use.local resolution: "@tests/namespaced-references@workspace:tests/analysis_tests/tests-namespaced-references"