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
8 changes: 4 additions & 4 deletions .drom

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

1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## [0.1.4] Next release

### Added
- Support for connecting to the LSP server remotely (TCP only) [#102](https://github.com/OCamlPro/superbol-studio-oss/pull/102)
- Support for Symbol Renaming command [#351](https://github.com/OCamlPro/superbol-studio-oss/pull/351)
- Show documentation comments on hover information [#350](https://github.com/OCamlPro/superbol-studio-oss/pull/350)
- Completion for more grammar constructs [#322](https://github.com/OCamlPro/superbol-studio-oss/pull/322)
Expand Down
1 change: 1 addition & 0 deletions dune-project

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

1 change: 1 addition & 0 deletions opam/vscode-languageclient-js-stubs.opam
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ depends: [
"dune" {>= "2.8.0"}
"vscode-js-stubs" {= version}
"promise_jsoo" {>= "0.3.1" & < "1.0.0"}
"node-js-stubs" {= version}
"jsonoo" {>= "0.2.1" & < "1.0.0"}
"js_of_ocaml" {>= "4"}
"interop-js-stubs" {= version}
Expand Down
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
10 changes: 10 additions & 0 deletions sphinx/remote-lsp.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
.. remote-lsp

You can simulate a remote LSP with the following script:
```
$ mkfifo /tmp/somewhere # possibly in a directory resulting from `mktemp -d`
$ nc -l 8000 < /tmp/somewhere | superbol-free lsp > /tmp/somewhere
```

Once this is running, you can enter `tcp://localhost:8000` in the `superbol.lsp-path` configuration setting. SuperBOL Studio will then rely on this LSP server instead of a binary that is bundled in the extension.
Note: this "made up" remote server terminates once the first connection is closed.
2 changes: 1 addition & 1 deletion src/lsp/superbol_free_lib/vscode_extension.ml
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ let package =
"vscode-languageclient", "8.0.2";
"polka", "^1.0.0-next.22";
"sirv", "^2.0.2";

(* for the debug extension: *)
"n-readlines", "^1.0.0";
]
Expand Down
3 changes: 3 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,9 @@ module Net : sig
module Socket : sig
type t

val t_of_js : Ojs.t -> t
val t_to_js : t -> Ojs.t

val make : unit -> t

val isPaused : t -> bool
Expand Down

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

Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ js_of_ocaml = ">=4"
jsonoo = "0.2.1"
promise_jsoo = "0.3.1"
vscode-js-stubs = "version"
node-js-stubs = "version"

# package tools dependencies
[tools]
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 -> Node.Net.Socket.t [@@js.get]

val reader : t -> Node.Net.Socket.t [@@js.get]

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

val create :
writer: Node.Net.Socket.t
-> reader: Node.Net.Socket.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 from_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 -> Node.Net.Socket.t [@@js.get]

val reader : t -> Node.Net.Socket.t [@@js.get]

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

val create :
writer: Node.Net.Socket.t
-> reader: Node.Net.Socket.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 from_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");
32 changes: 27 additions & 5 deletions src/vscode/superbol-vscode-platform/superbol_instance.ml
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,16 @@ 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
let subscribe_disposable { context; _ } disposable =
Vscode.ExtensionContext.subscribe context ~disposable

let stop_language_server t =
match t.language_client with
Expand All @@ -39,16 +46,31 @@ let start_language_server ({ context; _ } as t) =
let open Promise.Syntax in
let* () = stop_language_server t 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 Superbol_languageclient.server_access ~context with
| Sub_process serverOptions ->
LanguageClient.make () ~id ~name ~serverOptions
~clientOptions:(Superbol_languageclient.client_options ())
| TCP { host; port } ->
LanguageClient.from_stream ~id ~name begin fun () ->
let socket = Node.Net.Socket.(connect (make ())) ~host ~port in
Node.Net.Socket.on socket @@ `Connect begin fun () ->
subscribe_disposable t @@
Vscode.Window.setStatusBarMessage ()
~text:(Printf.sprintf "SuperBOL LSP client successfully connected \
to %s on port %u" host port)
~hide:(`AfterTimeout 10000)
end;
Promise.return @@
Vscode_languageclient.StreamInfo.create ()
~writer:socket
~reader:socket
end
in
let+ () = LanguageClient.start client in
t.language_client <- Some client



let current_document_uri ?text_editor () =
match
match text_editor with None -> Vscode.Window.activeTextEditor () | e -> e
Expand Down
1 change: 1 addition & 0 deletions src/vscode/superbol-vscode-platform/superbol_instance.mli
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ type t
type client = Vscode_languageclient.LanguageClient.t

val make: context:Vscode.ExtensionContext.t -> t
val subscribe_disposable: t -> Vscode.Disposable.t -> unit

val stop_language_server: t -> unit Promise.t
val start_language_server: t -> unit Promise.t
Expand Down
40 changes: 36 additions & 4 deletions src/vscode/superbol-vscode-platform/superbol_languageclient.ml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@

module LSP = Vscode_languageclient

type server_access =
| Sub_process of LSP.ServerOptions.t
| TCP of { host: string; port: int }


(* Helpers to find the bundled superbol executable *)
let rec find_existing = function
Expand Down Expand Up @@ -48,7 +52,26 @@ let find_superbol root =
]


let server_options ~context =
let scan_host_and_port url =
let fail () = Format.ksprintf failwith "Invalid %S" url in
match String.split_on_char ':' url with
| [host; port] ->
(try TCP { host; port = int_of_string port }
with Invalid_argument _ -> fail ())
| _ ->
fail ()

let scan_server_command cmd =
let prefix = "tcp://" 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 (scan_host_and_port url)
else
None


let server_command ~context ?cmd () =
let root_uri = Vscode.ExtensionContext.extensionUri context
and storage_uri =
(* Use the global state URI as the server stores caches on a per-project
Expand All @@ -59,7 +82,7 @@ let server_options ~context =
else None
in
let command =
match Superbol_workspace.superbol_exe () with
match cmd with
| Some cmd ->
cmd
| None ->
Expand All @@ -85,8 +108,17 @@ let server_options ~context =
| None -> []
| Some uri -> ["--storage-directory"; Vscode.Uri.fsPath uri])
in
LSP.ServerOptions.create ()
~options ~command ~args
Sub_process (LSP.ServerOptions.create ~options ~command ~args ())


let server_access ~context =
match Superbol_workspace.superbol_exe () with
| None ->
server_command ~context ()
| Some cmd ->
match scan_server_command cmd with
| Some access -> access
| None -> server_command ~context ~cmd ()


let client_options () =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,13 @@
(* *)
(**************************************************************************)

val server_options
type server_access =
| Sub_process of Vscode_languageclient.ServerOptions.t
| TCP of { host: string; port: int }

val server_access
: context: Vscode.ExtensionContext.t
-> Vscode_languageclient.ServerOptions.t
-> server_access

val client_options
: unit
Expand Down
32 changes: 14 additions & 18 deletions src/vscode/superbol-vscode-platform/superbol_vscode_platform.ml
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,20 @@
(* *)
(**************************************************************************)

let lsp_server_autorestarter instance : Vscode.Disposable.t =
let start_lsp_server_autorestarter instance =
Superbol_instance.subscribe_disposable instance @@
Vscode.Workspace.onDidChangeConfiguration ()
~listener:begin fun config_change ->
if Superbol_languageclient.server_needs_restart_after ~config_change
then
let _ =
Vscode.Window.setStatusBarMessage ()
~text:"Restarting SuperBOL Language Server…"
~hide:(`AfterTimeout 2000)
in
let _: unit Promise.t =
then begin
Superbol_instance.subscribe_disposable instance @@
Vscode.Window.setStatusBarMessage ()
~text:"Restarting SuperBOL Language Server…"
~hide:(`AfterTimeout 2000);
let _ : unit Promise.t =
Superbol_instance.start_language_server instance
in ()
else ()
end;
end

(* --- *)
Expand All @@ -36,15 +36,11 @@ let activate (extension: Vscode.ExtensionContext.t) =
let instance = Superbol_instance.make ~context:extension in
current_instance := Some instance;

let task =
Vscode.Tasks.registerTaskProvider
~type_:Superbol_tasks.type_
~provider:(Superbol_tasks.provider instance)
in
Vscode.ExtensionContext.subscribe extension ~disposable:task;

Vscode.ExtensionContext.subscribe extension
~disposable:(lsp_server_autorestarter instance);
Superbol_instance.subscribe_disposable instance @@
Vscode.Tasks.registerTaskProvider
~type_:Superbol_tasks.type_
~provider:(Superbol_tasks.provider instance);
start_lsp_server_autorestarter instance;

Superbol_commands.register_all extension instance;
Superbol_instance.start_language_server instance
Expand Down
Loading