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

Allowing remote LSP connection in vscode API #102

Merged
merged 15 commits into from
Sep 6, 2024
Merged
1 change: 1 addition & 0 deletions package.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions sphinx/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ Welcome to SuperBOL Studio OSS
commands
API doc <https://ocamlpro.github.io/superbol-studio-oss/doc>
license
remote-lsp

Sources and Issues on Github <https://github.com/ocamlpro/superbol-studio-oss>

Expand Down
25 changes: 25 additions & 0 deletions sphinx/remote-lsp.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
.. remote-lsp

You can simulate a remote LSP with the following scripts:

Start a server on any port (here I chose 8000):
`$ nc -k -l localhost 8000 > test.nc`

Then, use the following script to read on a file:
```
touch $1
inotifywait -m -e modify $1 | while read line
do
if [ ! -s $1 ]; then
echo 'Waiting for content'
else
cat $1 | superbol lsp > superbol.log
> $1
fi
done
```

and call it with `test.nc`.

TODO: this script only receives information, but does not send it back to
VSCODE; it only writes it in superbol.log. It should answer to the client.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that's quite a big TODO. What happens when you try this suggestion?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that's quite a big TODO.

The thing was, when I started this PR, the LSP did not have a HTTP server (compatible with VSCode) , hence neither these script actually make it work. This PR was initially only to provide the base before starting the actual server

1 change: 1 addition & 0 deletions src/lsp/superbol_free_lib/vscode_extension.ml
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ let package =
"vscode-languageclient", "8.0.2";
"polka", "^1.0.0-next.22";
"sirv", "^2.0.2";
"ws", "^8.14.2";

(* for the debug extension: *)
"n-readlines", "^1.0.0";
Expand Down
2 changes: 2 additions & 0 deletions src/vendor/vscode-ocaml-platform/src-bindings/node/node.ml
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,8 @@ module Net = struct
module Socket = struct
include Class.Make ()

let to_ojs (t : t) : Ojs.t = (t :> Ojs.t)

Stevendeo marked this conversation as resolved.
Show resolved Hide resolved
include
[%js:
val make : unit -> t [@@js.new "net.Socket"]
Expand Down
2 changes: 2 additions & 0 deletions src/vendor/vscode-ocaml-platform/src-bindings/node/node.mli
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,8 @@ module Net : sig
module Socket : sig
type t

val to_ojs : t -> Ojs.t

val make : unit -> t

val isPaused : t -> bool
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,26 @@ module StaticFeature = struct
[@@js.builder]]
end

module StreamInfo = struct
include Interface.Make ()

include
[%js:
val writer : t -> Ojs.t [@@js.get]

val reader : t -> Ojs.t [@@js.get]

val detached : t -> bool option [@@js.get]

val create :
writer:Ojs.t
-> reader:Ojs.t
-> ?detached:bool
-> unit
-> t
[@@js.builder]]
end

module DidChangeConfiguration = struct
include Interface.Make ()

Expand All @@ -211,6 +231,13 @@ module LanguageClient = struct
-> t
[@@js.new "vscode_languageclient.LanguageClient"]

val make_stream :
id:string
-> name:string
-> (unit -> StreamInfo.t Promise.t)
-> t
[@@js.new "vscode_languageclient.LanguageClient"]

val start : t -> unit Promise.t [@@js.call]

val isRunning : t -> bool [@@js.call]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,24 @@ module DidChangeConfiguration : sig
val create : settings:Ojs.t -> unit -> t
end

module StreamInfo: sig
include Js.T

val writer : t -> Ojs.t [@@js.get]

val reader : t -> Ojs.t [@@js.get]

val detached : t -> bool option [@@js.get]

val create :
writer:Ojs.t
-> reader:Ojs.t
-> ?detached:bool
-> unit
-> t
[@@js.builder]
end

module LanguageClient : sig
include Js.T

Expand All @@ -179,6 +197,12 @@ module LanguageClient : sig
-> unit
-> t

val make_stream :
id:string
-> name:string
-> (unit -> StreamInfo.t Promise.t)
-> t

val start : t -> unit Promise.t

val isRunning : t -> bool
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
joo_global_object.vscode_languageclient = require("vscode-languageclient");
// joo_global_object.webSocket = require("ws");
47 changes: 42 additions & 5 deletions src/vscode/superbol-vscode-platform/superbol_instance.ml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ type t = {
}
type client = LanguageClient.t


let id = "superbol-free-lsp"

let name = "SuperBOL Language Server"

let make ~context = { context; language_client = None }

let client { language_client; _ } = language_client
Expand All @@ -35,15 +40,47 @@ let stop_language_server t =
else
Promise.return ()

let is_remote_lsp =
let prefix = "tcp://" in
fun serverOpt ->
let cmd = Executable.command serverOpt in
if String.starts_with ~prefix cmd then
let l = String.length prefix in
let url = String.sub cmd l (String.length cmd - l) in
Some url
else
None

let host_and_port url =
let fail () = Format.ksprintf failwith "Invalid %S" url in
match String.split_on_char ':' url with
| [] | [_] -> fail ()
| host :: port :: _ ->
try host, int_of_string port with Invalid_argument _ -> fail ()

let start_language_server ({ context; _ } as t) =
let open Promise.Syntax in
let* () = stop_language_server t in
let serverOptions =
Superbol_languageclient.server_options ~context
in
let client =
LanguageClient.make ()
~id: "superbol-free-lsp"
~name: "SuperBOL Language Server"
~serverOptions:(Superbol_languageclient.server_options ~context)
~clientOptions:(Superbol_languageclient.client_options ())
match is_remote_lsp serverOptions with
| Some url ->
let host, port = host_and_port url in
LanguageClient.make_stream ~id ~name begin fun () ->
let socket =
(* Vscode_languageclient.StreamInfo.njs_stream_of_socket @@ *)
Node.Net.Socket.(to_ojs (connect (make ()) ~host ~port))
in
Promise.return @@
Vscode_languageclient.StreamInfo.create ()
~writer:socket
~reader:socket
end
| None ->
let clientOptions = Superbol_languageclient.client_options () in
LanguageClient.make () ~id ~name ~serverOptions ~clientOptions
in
let+ () = LanguageClient.start client in
t.language_client <- Some client
Expand Down
Loading