Skip to content

Commit c787b88

Browse files
committed
Recover the LSP analysis context after a workspace build
Why: Projects opened before their first build lack generated metadata. Recover on compiler-log events without requiring an editor restart. Context modified (compiler-log creation and change notifications): Before (text): Refresh diagnostics using the existing analysis and package context. After (text): Discover fresh analysis tables and package configuration. Replace the context if the workspace package is available. Otherwise retain the previous context without repeated error messages. Preserve open buffers, settings and diagnostics, then refresh diagnostics. Diagnostic modified (initial package discovery failure): Before (text): Try building the project then restart the server After (text): Try building the project. The server will retry when a compiler log changes. Add snapshot tests for Created and Changed events, early retries, compiled-file lookup and preservation of unsaved buffers. Document the automatic recovery behavior. Validation: dune build and eight recovery/diagnostic tests pass. The full LSP suite still has snapshot differences in hover_jsx.ml and code_action.ml. Signed-off-by: Pedro Castro <aspeddro@gmail.com>
1 parent 213dd40 commit c787b88

18 files changed

Lines changed: 406 additions & 156 deletions

‎.github/workflows/ci.yml‎

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -253,9 +253,6 @@ jobs:
253253
- name: Check for diffs in tests folder
254254
run: git diff --ignore-cr-at-eol --exit-code tests
255255

256-
- name: Run LSP tests
257-
run: opam exec -- make test-lsp
258-
259256
# On Windows, after running setup-ocaml (if it wasn't cached yet or the cache couldn't be restored),
260257
# Cygwin bash is used instead of Git Bash for Windows, breaking the test scripts below.
261258
# So we need to adjust the path to bring back Git Bash for Windows.

‎Makefile‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ clean-rewatch:
105105

106106
COMPILER_SOURCE_DIRS := compiler tests analysis tools rewatch-ocaml lsp
107107
COMPILER_SOURCES = $(shell find $(COMPILER_SOURCE_DIRS) -type f \( -name '*.ml' -o -name '*.mli' -o -name '*.c' -o -name '*.h' -o -name '*.dune' -o -name dune -o -name dune-project \))
108-
COMPILER_BIN_NAMES := bsc rescript-editor-analysis rescript-tools rescript rescript-language-server
108+
COMPILER_BIN_NAMES := bsc rescript-editor-analysis rescript-tools rescript
109109
COMPILER_EXES := $(addsuffix .exe,$(addprefix $(BIN_DIR)/,$(COMPILER_BIN_NAMES)))
110110

111111
compiler: $(COMPILER_EXES)

‎analysis/src/shared_types.ml‎

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -979,3 +979,108 @@ let extract_exp_apply_args ~args =
979979
| [] -> List.rev acc
980980
in
981981
args |> process_args ~acc:[]
982+
983+
let state_to_yojson (state : state) =
984+
let option_to_yojson f = function
985+
| None -> `Null
986+
| Some value -> f value
987+
in
988+
989+
let string_set_to_yojson set =
990+
`List (set |> File_set.elements |> List.map (fun value -> `String value))
991+
in
992+
993+
let path_to_yojson path = `List (List.map (fun item -> `String item) path) in
994+
995+
let paths_to_yojson = function
996+
| Impl {cmt; res} ->
997+
`Assoc
998+
[("kind", `String "Impl"); ("cmt", `String cmt); ("res", `String res)]
999+
| Namespace {cmt} ->
1000+
`Assoc [("kind", `String "Namespace"); ("cmt", `String cmt)]
1001+
| IntfAndImpl {cmti; resi; cmt; res} ->
1002+
`Assoc
1003+
[
1004+
("kind", `String "IntfAndImpl");
1005+
("cmti", `String cmti);
1006+
("resi", `String resi);
1007+
("cmt", `String cmt);
1008+
("res", `String res);
1009+
]
1010+
in
1011+
1012+
let paths_for_module_to_yojson paths_for_module =
1013+
paths_for_module |> Hashtbl.to_seq
1014+
|> Seq.map (fun (file, paths) -> (file, paths_to_yojson paths))
1015+
|> List.of_seq
1016+
|> fun fields -> `Assoc fields
1017+
in
1018+
1019+
let autocomplete_to_yojson autocomplete =
1020+
autocomplete |> Misc.String_map.bindings
1021+
|> List.map (fun (name, files) ->
1022+
(name, `List (List.map (fun file -> `String file) files)))
1023+
|> fun fields -> `Assoc fields
1024+
in
1025+
1026+
let package_to_yojson (package : package) =
1027+
let major, minor = package.rescript_version in
1028+
`Assoc
1029+
[
1030+
( "generic_jsx_module",
1031+
option_to_yojson
1032+
(fun value -> `String value)
1033+
package.generic_jsx_module );
1034+
("suffix", `String package.suffix);
1035+
("root_path", `String package.root_path);
1036+
("project_files", string_set_to_yojson package.project_files);
1037+
( "dependencies",
1038+
`List (package.dependencies |> List.map (fun x -> `String x)) );
1039+
("dependencies_files", string_set_to_yojson package.dependencies_files);
1040+
("paths_for_module", paths_for_module_to_yojson package.paths_for_module);
1041+
( "namespace",
1042+
option_to_yojson (fun value -> `String value) package.namespace );
1043+
("opens", `List (List.map path_to_yojson package.opens));
1044+
( "rescript_version",
1045+
`Assoc [("major", `Int major); ("minor", `Int minor)] );
1046+
("autocomplete", autocomplete_to_yojson package.autocomplete);
1047+
]
1048+
in
1049+
1050+
let file_to_yojson (file : File.t) =
1051+
`Assoc
1052+
[
1053+
("uri", `String (file.uri |> Lsp.Uri.to_string));
1054+
("module_name", `String file.module_name);
1055+
("stamps_count", `Int (List.length (Stamps.get_entries file.stamps)));
1056+
("structure_name", `String file.structure.name);
1057+
( "structure_docstring",
1058+
`List (List.map (fun value -> `String value) file.structure.docstring)
1059+
);
1060+
("structure_items_count", `Int (List.length file.structure.items));
1061+
]
1062+
in
1063+
1064+
let cmt_cache =
1065+
state.cmt_cache |> Hashtbl.to_seq
1066+
|> Seq.map (fun (file_path, file) -> (file_path, file_to_yojson file))
1067+
|> List.of_seq
1068+
in
1069+
1070+
let root_for_uri =
1071+
state.root_for_uri |> Hashtbl.to_seq |> List.of_seq
1072+
|> List.map (fun (uri, str) -> [(Lsp.Uri.to_string uri, `String str)])
1073+
|> List.flatten
1074+
in
1075+
1076+
let packages_by_root =
1077+
state.packages_by_root |> Hashtbl.to_seq |> List.of_seq
1078+
|> List.map (fun (root, package) -> (root, package_to_yojson package))
1079+
in
1080+
1081+
`Assoc
1082+
[
1083+
("cmt_cache", `Assoc cmt_cache);
1084+
("root_for_uri", `Assoc root_for_uri);
1085+
("packages_by_root", `Assoc packages_by_root);
1086+
]

‎analysis/src/uri.mli‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
type t = Lsp.Uri.t
2+
3+
val from_path : string -> t
4+
val from_string : string -> t
5+
val is_interface : t -> bool
6+
val strip_path : bool ref
7+
val to_path : t -> string
8+
val to_string : t -> string
9+
val to_top_level_loc : t -> Location.t
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
- Update ppx_expect to v0.17.3 to resolve test dependencies with ppxlib 0.38 and OCaml 5.5.

‎dune-project‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@
8484
(synopsis "ReScript LSP")
8585
(depends
8686
(ocaml
87-
(>= 4.10))
87+
(>= 5.0.0))
8888
(lsp
8989
(>= 1.22.0))
9090
(eio
@@ -95,4 +95,4 @@
9595
dune
9696
ppx_deriving_yojson
9797
(ppx_expect
98-
(and :with-test (= v0.17.2)))))
98+
(and :with-test (= v0.17.3)))))

‎lsp/lsp.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,11 @@ notifications. Some are out of scope because they don't make sense.
114114
generated with `build_root` field for each subpackage.
115115
- [x] Add the warning number to the diagnostic message.
116116
- [x] `workspace/didChangeWatchedFiles` - client notification
117+
- When a `.compiler.log` is created or changed, the server rediscovers packages
118+
and refreshes its analysis context and compiler configuration. A workspace
119+
opened before its first build can recover without restarting the server.
120+
Failed discovery leaves the previous context intact; open buffers, settings
121+
and diagnostic ownership are preserved.
117122
- The server uses this notification to detect changes to generated
118123
`.compiler.log` files, which usually means a ReScript build has finished.
119124
When a watched log changes, the server re-reads every known compiler log and

‎lsp/src/code_actions.ml‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ end = struct
7979
let find_did_you_mean_line message =
8080
message |> String.split_on_char '\n'
8181
|> List.find_opt (fun line ->
82-
String.starts_with ~prefix:did_you_mean_prefix (String.trim line))
82+
String.starts_with ~prefix:did_you_mean_prefix (String.trim line))
8383
in
8484

8585
let message = diagnostic_message diagnostic in
@@ -323,10 +323,10 @@ end = struct
323323
let fields =
324324
record_field_names
325325
|> List.mapi (fun index field_name ->
326-
let padding =
327-
if index = 0 then " " else padding_content_record_field
328-
in
329-
Printf.sprintf "%s%s: %s,\n" padding field_name todo_value)
326+
let padding =
327+
if index = 0 then " " else padding_content_record_field
328+
in
329+
Printf.sprintf "%s%s: %s,\n" padding field_name todo_value)
330330
|> String.concat ""
331331
in
332332
fields ^ padding_content_end_brace
@@ -338,7 +338,7 @@ end = struct
338338
let fields =
339339
record_field_names
340340
|> List.map (fun field_name ->
341-
Printf.sprintf "%s: %s" field_name todo_value)
341+
Printf.sprintf "%s: %s" field_name todo_value)
342342
|> String.concat ", "
343343
in
344344
prefix ^ fields

‎lsp/src/compiler_config.ml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ module Parse = struct
102102
let r =
103103
specs
104104
|> List.map (fun spec ->
105-
package_spec_to_yojson spec |> Yojson.Safe.pretty_to_string)
105+
package_spec_to_yojson spec |> Yojson.Safe.pretty_to_string)
106106
|> String.concat ", "
107107
in
108108
Printf.sprintf "package_specs=Some(%s)" r |> print_endline

‎lsp/src/compiler_log.ml‎

Lines changed: 30 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -374,12 +374,12 @@ end = struct
374374
let entries =
375375
paths
376376
|> List.map (fun filepath ->
377-
{
378-
path = filepath_of_path filepath;
379-
kind = Circular_dependency;
380-
message;
381-
range = None;
382-
})
377+
{
378+
path = filepath_of_path filepath;
379+
kind = Circular_dependency;
380+
message;
381+
range = None;
382+
})
383383
in
384384
loop (next_i + 1) (List.rev_append entries acc)
385385
else if starts_with "FAILED: dependency cycle:" trimmed then
@@ -388,12 +388,12 @@ end = struct
388388
let entries =
389389
paths
390390
|> List.map (fun filepath ->
391-
{
392-
path = filepath_of_path filepath;
393-
kind = Circular_dependency;
394-
message;
395-
range = None;
396-
})
391+
{
392+
path = filepath_of_path filepath;
393+
kind = Circular_dependency;
394+
message;
395+
range = None;
396+
})
397397
in
398398
loop (i + 1) (List.rev_append entries acc)
399399
else loop (i + 1) acc
@@ -516,24 +516,24 @@ let%expect_test "parse log" =
516516
let print_logs logs =
517517
logs
518518
|> List.iter (fun (entry : Parse.diagnostic_entry) ->
519-
let path =
520-
match entry.path with
521-
| Parse.Relative_path p -> Printf.sprintf "Relative_path(%s)" p
522-
| Full_path p -> Printf.sprintf "Full_path(%s)" p
523-
in
524-
print_endline
525-
((match entry.kind with
526-
| Syntax_error -> "Syntax_error"
527-
| Warning {number; configured_as_error} ->
528-
if configured_as_error then
529-
Printf.sprintf "Warning %d (configured as error)" number
530-
else Printf.sprintf "Warning %d" number
531-
| Unknown -> "Unknown"
532-
| Circular_dependency -> "Circular_dependency"
533-
| Common_error -> "Common_error")
534-
^ " - " ^ path);
535-
diagnostic_of_entry entry |> print_endline;
536-
print_newline ())
519+
let path =
520+
match entry.path with
521+
| Parse.Relative_path p -> Printf.sprintf "Relative_path(%s)" p
522+
| Full_path p -> Printf.sprintf "Full_path(%s)" p
523+
in
524+
print_endline
525+
((match entry.kind with
526+
| Syntax_error -> "Syntax_error"
527+
| Warning {number; configured_as_error} ->
528+
if configured_as_error then
529+
Printf.sprintf "Warning %d (configured as error)" number
530+
else Printf.sprintf "Warning %d" number
531+
| Unknown -> "Unknown"
532+
| Circular_dependency -> "Circular_dependency"
533+
| Common_error -> "Common_error")
534+
^ " - " ^ path);
535+
diagnostic_of_entry entry |> print_endline;
536+
print_newline ())
537537
in
538538
let example_log_1 =
539539
{|

0 commit comments

Comments
 (0)