Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
693422c
initial lsp
aspeddro Dec 1, 2024
34bcd22
Merge branch 'master' into lsp
aspeddro Dec 30, 2024
7f209ef
Merge branch 'master' into lsp
aspeddro Mar 17, 2025
e203450
Merge branch 'master' into lsp
aspeddro Jun 20, 2025
e1273d6
Merge branch 'master' into lsp
aspeddro Apr 28, 2026
129af21
update
aspeddro Apr 29, 2026
6b65a25
add state
aspeddro Apr 30, 2026
e2989d0
Extract server I/O into Server module and simplify handlers
aspeddro May 12, 2026
a99abc6
Add parse_implementation_from_source and refactor LSP server
aspeddro May 12, 2026
2529f30
Merge branch 'master' into lsp
aspeddro May 13, 2026
365836c
Split LSP into bin/src layout and add hover integration test
aspeddro May 14, 2026
1692629
Refactor analysis to decouple I/O from core logic
aspeddro May 16, 2026
743126f
Delegate hover logic to `Analysis.Commands.hover`
aspeddro May 16, 2026
0e522be
Rename `tokenModifiersString` to `tokenModifiers` with correct type
aspeddro May 16, 2026
9da83c8
Merge branch 'master' into lsp
aspeddro May 22, 2026
9df9079
Update dune-project
aspeddro May 22, 2026
1b65e21
Merge branch 'master' into lsp
aspeddro May 30, 2026
768a040
Merge branch 'master' into lsp
aspeddro May 31, 2026
29cd3d7
Refactor LSP test infrastructure
aspeddro May 31, 2026
a20da0d
ci: add linux-headers
aspeddro May 31, 2026
5a57bbc
ci: add linux-headers-generic
aspeddro May 31, 2026
9442c4b
ci: linux-libc-dev
aspeddro May 31, 2026
64dffd8
ci: add build-essential
aspeddro May 31, 2026
e0f6296
ci: add build-essential and linux-libc-dev
aspeddro May 31, 2026
8cebaf2
ci: update cache version to v5
aspeddro May 31, 2026
85138b1
ci: make Linux headers visible to musl-gcc
aspeddro Jun 1, 2026
ee6de86
Merge branch 'master' into lsp
aspeddro Jun 1, 2026
66b69fe
ci: support arm and disable test for build for ocaml 5.0
aspeddro Jun 1, 2026
c50f613
makefile: add test-lsp
aspeddro Jun 2, 2026
fd83f06
Merge branch 'master' into lsp
aspeddro Jun 3, 2026
890fd16
Add LSP compiler diagnostics and file watching
aspeddro Jun 5, 2026
39639c6
Comment to add more compiler log test
aspeddro Jun 6, 2026
5e002cb
ci: add lsp test step
aspeddro Jun 6, 2026
5781622
fix opam install ppx_expect
aspeddro Jun 6, 2026
728c4bf
ci: run test inside opam `opam exec --`
aspeddro Jun 6, 2026
33edc85
lsp: disable hover supports_markdown_links for now
aspeddro Jun 6, 2026
75a6123
Move Eio_main setup to LSP executable
aspeddro Jun 6, 2026
126f67a
ci: skip lsp tests on Windows
aspeddro Jun 6, 2026
5d14a57
Expand LSP request handling and diagnostics
aspeddro Jun 7, 2026
015a2d2
Enforce LSP server lifecycle
aspeddro Jun 7, 2026
bbcb7b9
analysis refactor: remove global state `Shared_types.state`
aspeddro Jun 7, 2026
b63a60f
lsp: use analysis state
aspeddro Jun 7, 2026
88190b9
Revert "analysis refactor: remove global state `Shared_types.state`"
aspeddro Jun 8, 2026
4632fab
Merge branch 'master' into lsp
aspeddro Jun 8, 2026
3be179c
analysis: add state_to_yojson
aspeddro Jun 8, 2026
9bc661c
lsp: refactor load_full
aspeddro Jun 8, 2026
8ec52ae
add minimal param to State.to_yojson
aspeddro Jun 9, 2026
22a5eeb
Makefile: skip test on Windows and update ci
aspeddro Jun 9, 2026
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
70 changes: 63 additions & 7 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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}}

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 <linux/...> 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 <linux/swab.h>.
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 <linux/swab.h>' > /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'
Expand Down Expand Up @@ -326,6 +377,11 @@ jobs:
if: runner.os != 'Windows'
run: make -C tests/gentype_tests/typescript-react-example clean test

# The Makefile skip some tests on Windows because OCaml Eio process operations is not supported on Windows yet
# Eio.Stdenv.process_mgr raise a error, see https://github.com/ocaml-multicore/eio/blob/37d6e67f7e25b43e4a66574ed98838c79f1a21b4/lib_eio_windows/eio_windows.ml#L36
- name: Run LSP tests
run: opam exec -- make test-lsp

# On Windows, after running setup-ocaml (if it wasn't cached yet or the cache couldn't be restored),
# Cygwin bash is used instead of Git Bash for Windows, breaking the rewatch tests.
# So we need to adjust the path to bring back Git Bash for Windows.
Expand Down Expand Up @@ -604,7 +660,7 @@ jobs:
uses: actions/checkout@v6
- name: Run make in dev container
uses: devcontainers/ci@v0.3
with:
with:
push: never
runCmd: make

Expand Down
20 changes: 19 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,25 @@ test-gentype: lib
test-rewatch: lib
./rewatch/tests/suite.sh $(RESCRIPT_EXE)

test-all: test test-gentype test-analysis test-tools test-rewatch
test-lsp: lib
@for dir in tests/lsp_tests/*-workspace/; do \
[ -d "$$dir" ] || continue; \
echo "Building $${dir%/}..."; \
( cd "$$dir" && yarn clean && yarn build ); \
done
@dune runtest
@if [ "$$OS" = "Windows_NT" ]; then \
echo "Skipping lsp-tests executable on Windows"; \
else \
dune exec -- lsp-tests; \
fi
@if [ -n "$$(git ls-files --modified tests/lsp_tests/**/*.expected)" ]; then \
echo "The lsp_tests snapshot doesn't match. Double check that the output is correct, run 'make test-lsp' and stage the diff"; \
git --no-pager diff tests/lsp_tests/**/*.expected; \
exit 1; \
fi \

test-all: test test-gentype test-analysis test-tools test-rewatch test-lsp

# Playground

Expand Down
2 changes: 1 addition & 1 deletion analysis/bin/main.ml
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ let main () =
Cli.type_definition ~state ~path
~pos:(int_of_string line, int_of_string col)
~debug
| [_; "documentSymbol"; path] -> Document_symbol.command ~path
| [_; "documentSymbol"; path] -> Cli.document_symbol ~path
| [_; "hover"; path; line; col; current_file; supports_markdown_links] ->
Cli.hover ~state ~path
~pos:(int_of_string line, int_of_string col)
Expand Down
11 changes: 10 additions & 1 deletion analysis/src/cli.ml
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,15 @@ let semantic_tokens ~path =
let tokens = Semantic_tokens.semantic_tokens ~source ~kind_file in
Lsp.Types.SemanticTokens.yojson_of_t tokens |> print_string

let document_symbol ~path =
match Files.read_file path with
| None -> print_null ()
| Some source ->
let kind_file = Files.classify_source_file path in
Document_symbol.get_symbols ~source ~kind_file
|> List.map Lsp.Types.DocumentSymbol.yojson_of_t
|> print_list

let test ~state ~path =
Uri.strip_path := true;
match Files.read_file path with
Expand Down Expand Up @@ -247,7 +256,7 @@ let test ~state ~path =
Dce_command.command ()
| "doc" ->
print_endline ("DocumentSymbol " ^ path);
Document_symbol.command ~path
document_symbol ~path
| "hig" ->
print_endline ("Highlight " ^ path);
let source = Files.read_file path |> Option.get in
Expand Down
20 changes: 8 additions & 12 deletions analysis/src/document_symbol.ml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
(* https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_documentSymbol *)

let command ~path =
let get_symbols ~source ~kind_file =
let symbols = ref [] in
let add_symbol name loc kind =
if
Expand Down Expand Up @@ -115,17 +115,18 @@ let command ~path =
}
in

(if Filename.check_suffix path ".res" then
(if kind_file = Files.Res then
let parser =
Res_driver.parsing_engine.parse_implementation ~for_printer:false
Res_driver.parsing_engine.parse_implementation_from_source
~for_printer:false
in
let {Res_driver.parsetree = structure} = parser ~filename:path in
let {Res_driver.parsetree = structure} = parser ~source in
iterator.structure iterator structure |> ignore
else
let parser =
Res_driver.parsing_engine.parse_interface ~for_printer:false
Res_driver.parsing_engine.parse_interface_from_source ~for_printer:false
in
let {Res_driver.parsetree = signature} = parser ~filename:path in
let {Res_driver.parsetree = signature} = parser ~source in
iterator.signature iterator signature |> ignore);
let is_inside
({
Expand Down Expand Up @@ -182,9 +183,4 @@ let command ~path =
|> add_sorted_symbols_to_children ~sorted_symbols:rest
in
let sorted_symbols = !symbols |> List.sort compare_symbol in
let symbols_with_children =
[] |> add_sorted_symbols_to_children ~sorted_symbols
in
`List (symbols_with_children |> List.map Lsp.Types.DocumentSymbol.yojson_of_t)
|> Yojson.Safe.pretty_to_string ~std:true
|> print_endline
[] |> add_sorted_symbols_to_children ~sorted_symbols
103 changes: 103 additions & 0 deletions analysis/src/shared_types.ml
Original file line number Diff line number Diff line change
Expand Up @@ -965,3 +965,106 @@ let extract_exp_apply_args ~args =
| [] -> List.rev acc
in
args |> process_args ~acc:[]

let state_to_yojson (state : state) =
let option_to_yojson f = function
| None -> `Null
| Some value -> f value
in

let string_set_to_yojson set =
`List (set |> File_set.elements |> List.map (fun value -> `String value))
in

let path_to_yojson path = `List (List.map (fun item -> `String item) path) in

let paths_to_yojson = function
| Impl {cmt; res} ->
`Assoc
[("kind", `String "Impl"); ("cmt", `String cmt); ("res", `String res)]
| Namespace {cmt} ->
`Assoc [("kind", `String "Namespace"); ("cmt", `String cmt)]
| IntfAndImpl {cmti; resi; cmt; res} ->
`Assoc
[
("kind", `String "IntfAndImpl");
("cmti", `String cmti);
("resi", `String resi);
("cmt", `String cmt);
("res", `String res);
]
in

let paths_for_module_to_yojson paths_for_module =
paths_for_module |> Hashtbl.to_seq
|> Seq.map (fun (file, paths) -> (file, paths_to_yojson paths))
|> List.of_seq
|> fun fields -> `Assoc fields
in

let autocomplete_to_yojson autocomplete =
autocomplete |> Misc.String_map.bindings
|> List.map (fun (name, files) ->
(name, `List (List.map (fun file -> `String file) files)))
|> fun fields -> `Assoc fields
in

let package_to_yojson (package : package) =
let major, minor = package.rescript_version in
`Assoc
[
( "generic_jsx_module",
option_to_yojson
(fun value -> `String value)
package.generic_jsx_module );
("suffix", `String package.suffix);
("root_path", `String package.root_path);
("project_files", string_set_to_yojson package.project_files);
("dependencies_files", string_set_to_yojson package.dependencies_files);
("paths_for_module", paths_for_module_to_yojson package.paths_for_module);
( "namespace",
option_to_yojson (fun value -> `String value) package.namespace );
("opens", `List (List.map path_to_yojson package.opens));
( "rescript_version",
`Assoc [("major", `Int major); ("minor", `Int minor)] );
("autocomplete", autocomplete_to_yojson package.autocomplete);
]
in

let file_to_yojson (file : File.t) =
`Assoc
[
("uri", `String (file.uri |> Lsp.Uri.to_string));
("module_name", `String file.module_name);
("stamps_count", `Int (List.length (Stamps.get_entries file.stamps)));
("structure_name", `String file.structure.name);
( "structure_docstring",
`List (List.map (fun value -> `String value) file.structure.docstring)
);
("structure_items_count", `Int (List.length file.structure.items));
]
in

let cmt_cache =
state.cmt_cache |> Hashtbl.to_seq
|> Seq.map (fun (file_path, file) -> (file_path, file_to_yojson file))
|> List.of_seq
in

let root_for_uri =
state.root_for_uri |> Hashtbl.to_seq |> List.of_seq
|> List.map (fun (uri, str) -> [(Lsp.Uri.to_string uri, `String str)])
|> List.flatten
in

let packages_by_root =
state.packages_by_root |> Hashtbl.to_seq |> List.of_seq
|> List.map (fun (root, package) -> (root, package_to_yojson package))
in

`Assoc
[
("cmt_cache", `Assoc cmt_cache);
("root_for_uri", `Assoc root_for_uri);
("packages_by_root", `Assoc packages_by_root);
]
2 changes: 1 addition & 1 deletion dune
Original file line number Diff line number Diff line change
@@ -1 +1 @@
(dirs compiler tests analysis tools)
(dirs compiler tests analysis tools lsp)
17 changes: 17 additions & 0 deletions dune-project
Original file line number Diff line number Diff line change
Expand Up @@ -75,3 +75,20 @@
(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
(ppx_expect
(and :with-test (= v0.17.2)))))
5 changes: 5 additions & 0 deletions lsp/bin/dune
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
(executable
(name main)
(package rescript-language-server)
(public_name rescript-language-server)
(libraries rescript_language_server eio_main))
6 changes: 6 additions & 0 deletions lsp/bin/main.ml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
let () =
Eio_main.run (fun env ->
let fs = Eio.Stdenv.fs env in
let stdin = Eio.Stdenv.stdin env in
let stdout = Eio.Stdenv.stdout env in
Rescript_language_server.listen ~input:stdin ~output:stdout ~fs)
Empty file added lsp/bin/main.mli
Empty file.
29 changes: 29 additions & 0 deletions lsp/src/compiler.ml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
module Uri_map = Map.Make (Lsp.Uri)

let collect_diagnostics_from_log_file path =
let content = Eio.Path.load path in
Compiler_log.Parse.parse_log_content content

let collect_diagnostics_from_log_using_source_dirs workspace_root
(state : State.t) =
let ( // ) = Filename.concat in
let ( /// ) = Eio.Path.( / ) in
let workspace_root_path = workspace_root |> Lsp.Types.DocumentUri.to_path in
let path =
workspace_root_path // Constants.compiler_dir_partial_path
// Constants.sources_dirs
in
let build_roots = Source_dirs.get_build_roots_from_file (state.fs /// path) in
let diagnostics =
match build_roots with
| Some build_roots ->
build_roots
|> List.map (fun build_root ->
let compiler_log_path =
workspace_root_path // build_root // Constants.compiler_log
in
collect_diagnostics_from_log_file (state.fs /// compiler_log_path))
|> List.flatten
| None -> []
in
diagnostics
Loading
Loading