Skip to content

Commit 961b071

Browse files
authored
Better snapshot (#7409)
* Use Vitest to have individual snapshot files * Show completable item in code fence * Update existing tests
1 parent 5f045f1 commit 961b071

15 files changed

+1215
-42
lines changed

analysis/bin/main.ml

+5-4
Original file line numberDiff line numberDiff line change
@@ -130,11 +130,12 @@ let main () =
130130
| [_; "completion"; path; line; col; currentFile] ->
131131
printHeaderInfo path line col;
132132
if !Cfg.useRevampedCompletion then
133-
Commands.completionRevamped ~debug ~path
133+
let source = Files.readFile currentFile in
134+
Commands.completionRevamped ~source ~debug ~path
134135
~pos:(int_of_string line, int_of_string col)
135136
~currentFile
136137
else
137-
Commands.completion ~debug ~path
138+
Commands.completion ~debug:true ~path
138139
~pos:(int_of_string line, int_of_string col)
139140
~currentFile
140141
| [_; "completionResolve"; path; modulePath] ->
@@ -213,11 +214,11 @@ let main () =
213214
(Json.escape (CreateInterface.command ~path ~cmiFile))
214215
| [_; "format"; path] ->
215216
Printf.printf "\"%s\"" (Json.escape (Commands.format ~path))
216-
| [_; "test"; path] -> Commands.test ~path
217+
| [_; "test"; path] -> Commands.test ~path ~debug
217218
| [_; "test_revamped"; path; config_file_path] ->
218219
Packages.overrideConfigFilePath := Some config_file_path;
219220
Cfg.useRevampedCompletion := true;
220-
Commands.test ~path
221+
Commands.test ~path ~debug
221222
| args when List.mem "-h" args || List.mem "--help" args -> prerr_endline help
222223
| _ ->
223224
prerr_endline help;

analysis/src/CodeFence.ml

+125
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
(* Define a type for a range with start and finish indices *)
2+
type range = {start: int; finish: int}
3+
4+
(* --- Helper function to find the 0-based line index containing a given 0-based character index --- *)
5+
let get_line_index_from_char_index code char_index =
6+
let lines = String.split_on_char '\n' code in
7+
let rec find_line_idx current_char_idx current_line_num remaining_lines =
8+
match remaining_lines with
9+
| [] ->
10+
max 0 (current_line_num - 1)
11+
(* If char_index is beyond the end, return last line index *)
12+
| line :: tl ->
13+
let line_length = String.length line in
14+
(* Check if char_index is within the current line (including the newline char) *)
15+
if
16+
char_index >= current_char_idx
17+
&& char_index <= current_char_idx + line_length
18+
then current_line_num
19+
else
20+
(* Move to the next line, account for the newline character (+1) *)
21+
find_line_idx
22+
(current_char_idx + line_length + 1)
23+
(current_line_num + 1) tl
24+
in
25+
find_line_idx 0 0 lines
26+
27+
(* --- Helper function to calculate the 0-based character index of the start of a given 0-based line index --- *)
28+
let get_char_index_from_line_index code target_line_index =
29+
let lines = String.split_on_char '\n' code in
30+
let rec calculate_start_index_impl current_char_idx current_line_num
31+
lines_to_process =
32+
if current_line_num >= target_line_index then current_char_idx
33+
else
34+
match lines_to_process with
35+
| [] -> current_char_idx (* Target line index is out of bounds *)
36+
| line :: tl ->
37+
(* Move past the current line and its newline character *)
38+
calculate_start_index_impl
39+
(current_char_idx + String.length line + 1)
40+
(current_line_num + 1) tl
41+
in
42+
calculate_start_index_impl 0 0 lines
43+
44+
(* --- Main formatting function --- *)
45+
let format_code_snippet_cropped code (underline_range : range option)
46+
lines_around_annotation =
47+
let lines = String.split_on_char '\n' code in
48+
let total_lines = List.length lines in
49+
let formatted_output = Buffer.create (String.length code) in
50+
(* Initial capacity *)
51+
52+
(* Determine the central line index for cropping *)
53+
let target_line_index =
54+
match underline_range with
55+
| Some {start; finish = _} -> get_line_index_from_char_index code start
56+
| None -> 0 (* Default to first line if no annotations *)
57+
in
58+
59+
(* Determine the cropping window (0-based line indices) *)
60+
let start_line_index = max 0 (target_line_index - lines_around_annotation) in
61+
let end_line_index =
62+
min (total_lines - 1) (target_line_index + lines_around_annotation)
63+
in
64+
65+
(* Keep track of the global character index corresponding to the start of the *current* line being iterated over *)
66+
let current_char_index = ref 0 in
67+
68+
(* Iterate through all original lines to correctly track current_char_index *)
69+
List.iteri
70+
(fun original_line_idx line ->
71+
let line_length = String.length line in
72+
(* Check if the current original line is within our cropping window *)
73+
if
74+
original_line_idx >= start_line_index
75+
&& original_line_idx <= end_line_index
76+
then (
77+
let original_line_number = original_line_idx + 1 in
78+
(* 1-based for display *)
79+
let line_number_prefix = Printf.sprintf "%d + " original_line_number in
80+
let prefix_length = String.length line_number_prefix in
81+
82+
(* Add the code line *)
83+
Buffer.add_string formatted_output line_number_prefix;
84+
Buffer.add_string formatted_output line;
85+
Buffer.add_char formatted_output '\n';
86+
87+
(* Prepare the annotation line buffer *)
88+
let annotation_line_buffer =
89+
Buffer.create (prefix_length + line_length)
90+
in
91+
Buffer.add_string annotation_line_buffer (String.make prefix_length ' ');
92+
93+
(* Initial padding *)
94+
let has_annotation_on_this_line = ref false in
95+
96+
(* Check each character position within this line for annotations *)
97+
for i = 0 to line_length - 1 do
98+
let global_char_index = !current_char_index + i in
99+
let annotation_char = ref ' ' in
100+
(* Default to space *)
101+
102+
(* Check for underline using Option.iter *)
103+
Option.iter
104+
(fun {start; finish} ->
105+
if global_char_index >= start && global_char_index < finish then (
106+
annotation_char := '-' (* '¯' *);
107+
(* Macron symbol *)
108+
has_annotation_on_this_line := true))
109+
underline_range;
110+
111+
Buffer.add_char annotation_line_buffer !annotation_char
112+
done;
113+
114+
(* Add the annotation line to the main output if needed *)
115+
if !has_annotation_on_this_line then (
116+
Buffer.add_buffer formatted_output annotation_line_buffer;
117+
Buffer.add_char formatted_output '\n'));
118+
119+
(* Update the global character index to the start of the next line *)
120+
(* This happens regardless of whether the line was in the cropped window *)
121+
current_char_index := !current_char_index + line_length + 1
122+
(* +1 for the newline *))
123+
lines;
124+
125+
Buffer.contents formatted_output

analysis/src/Commands.ml

+10-6
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
let completion ~debug ~path ~pos ~currentFile =
1+
let completion ~(debug : bool) ~path ~pos ~currentFile =
22
let completions =
33
match
4-
Completions.getCompletions ~debug ~path ~pos ~currentFile ~forHover:false
4+
Completions.getCompletions debug ~path ~pos ~currentFile ~forHover:false
55
with
66
| None -> []
77
| Some (completions, full, _) ->
@@ -11,9 +11,11 @@ let completion ~debug ~path ~pos ~currentFile =
1111
in
1212
completions |> Protocol.array |> print_endline
1313

14-
let completionRevamped ~debug ~path ~pos ~currentFile =
14+
let completionRevamped ?(source = None) ~debug ~path ~pos ~currentFile =
1515
let completions =
16-
match Completions.getCompletionsRevamped ~debug ~path ~pos ~currentFile with
16+
match
17+
Completions.getCompletionsRevamped ~source ~debug ~path ~pos ~currentFile
18+
with
1719
| None -> []
1820
| Some (completions, full, _) ->
1921
completions
@@ -313,7 +315,7 @@ let format ~path =
313315
let diagnosticSyntax ~path =
314316
print_endline (Diagnostics.document_syntax ~path |> Protocol.array)
315317

316-
let test ~path =
318+
let test ~path ~debug =
317319
Uri.stripPath := true;
318320
match Files.readFile path with
319321
| None -> assert false
@@ -383,7 +385,9 @@ let test ~path =
383385
^ string_of_int col);
384386
let currentFile = createCurrentFile () in
385387
if !Cfg.useRevampedCompletion then
386-
completionRevamped ~debug:true ~path ~pos:(line, col) ~currentFile
388+
let source = Files.readFile currentFile in
389+
completionRevamped ~source ~debug ~path ~pos:(line, col)
390+
~currentFile
387391
else completion ~debug:true ~path ~pos:(line, col) ~currentFile;
388392
Sys.remove currentFile
389393
| "cre" ->

analysis/src/Completions.ml

+27-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
let getCompletions ~debug ~path ~pos ~currentFile ~forHover =
1+
let getCompletions (debug : bool) ~path ~pos ~currentFile ~forHover =
22
let textOpt = Files.readFile currentFile in
33
match textOpt with
44
| None | Some "" -> None
@@ -21,7 +21,7 @@ let getCompletions ~debug ~path ~pos ~currentFile ~forHover =
2121
in
2222
Some (completables, full, scope)))
2323

24-
let getCompletionsRevamped ~debug ~path ~pos ~currentFile =
24+
let getCompletionsRevamped ?(source = None) ~debug ~path ~pos ~currentFile =
2525
let textOpt = Files.readFile currentFile in
2626
match textOpt with
2727
| None | Some "" -> None
@@ -30,8 +30,32 @@ let getCompletionsRevamped ~debug ~path ~pos ~currentFile =
3030
CompletionFrontEndRevamped.completionWithParser ~debug ~path
3131
~posCursor:pos ~currentFile ~text
3232
with
33-
| None -> None
33+
| None ->
34+
source
35+
|> Option.iter (fun _ ->
36+
print_endline "Completion Frontend did not return completable");
37+
None
3438
| Some (completable, scope) -> (
39+
let _ =
40+
match source with
41+
| Some text -> (
42+
match SharedTypes.CompletableRevamped.try_loc completable with
43+
| Some loc ->
44+
let range =
45+
CodeFence.
46+
{
47+
start = loc.Location.loc_start.pos_cnum;
48+
finish = loc.Warnings.loc_end.pos_cnum;
49+
}
50+
in
51+
Printf.printf "Found Completable: %s\n\n"
52+
(SharedTypes.CompletableRevamped.toString completable);
53+
CodeFence.format_code_snippet_cropped text (Some range) 3
54+
|> print_endline
55+
| None -> ())
56+
| None -> ()
57+
in
58+
3559
(* Only perform expensive ast operations if there are completables *)
3660
match Cmt.loadFullCmtFromPath ~path with
3761
| None -> None

analysis/src/Hover.ml

+1-1
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ let hoverWithExpandedTypes ~file ~package ~supportsMarkdownLinks typ =
150150
makes it (most often) work with unsaved content. *)
151151
let getHoverViaCompletions ~debug ~path ~pos ~currentFile ~forHover
152152
~supportsMarkdownLinks =
153-
match Completions.getCompletions ~debug ~path ~pos ~currentFile ~forHover with
153+
match Completions.getCompletions debug ~path ~pos ~currentFile ~forHover with
154154
| None -> None
155155
| Some (completions, ({file; package} as full), scope) -> (
156156
let rawOpens = Scope.getRawOpens scope in

analysis/src/SharedTypes.ml

+15
Original file line numberDiff line numberDiff line change
@@ -793,6 +793,21 @@ module CompletableRevamped = struct
793793
| CextensionNode of string
794794
| Cdecorator of string
795795
| CdecoratorPayload of decoratorPayload
796+
797+
let toString (t : t) =
798+
match t with
799+
| Cexpression _ -> "Cexpression"
800+
| Cpattern _ -> "Cpattern"
801+
| Cnone -> "Cnone"
802+
| CextensionNode _ -> "CextensionNode"
803+
| Cdecorator _ -> "Cdecorator"
804+
| CdecoratorPayload _ -> "CdecoratorPayload"
805+
806+
let try_loc (t : t) =
807+
match t with
808+
| Cexpression {typeLoc; _} -> Some typeLoc
809+
| Cpattern {typeLoc; _} -> Some typeLoc
810+
| _ -> None
796811
end
797812

798813
module ScopeTypes = struct

tests/analysis_new_tests/tests/package.json

+5-2
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,14 @@
55
"scripts": {
66
"build": "rescript",
77
"clean": "rescript clean -with-deps",
8-
"test": "yarn build && node test.js",
9-
"test:update": "node --test-update-snapshots test.js"
8+
"test": "yarn build && vitest run test.js",
9+
"test:update": "vitest run -u test.js"
1010
},
1111
"dependencies": {
1212
"@rescript/react": "link:../../dependencies/rescript-react",
1313
"rescript": "workspace:^"
14+
},
15+
"devDependencies": {
16+
"vitest": "3.1.2"
1417
}
1518
}

tests/analysis_new_tests/tests/test.js renamed to tests/analysis_new_tests/tests/snapshots.test.js

+10-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { test } from "node:test";
1+
import { test, expect } from "vitest";
22
import fs from "node:fs/promises";
33
import path from "node:path";
44
import { glob } from "glob";
@@ -116,7 +116,7 @@ await Promise.all(
116116
resFiles.forEach((file) => {
117117
const blockData = testBlocksPerFile.get(file.relativePath);
118118
for (const block of blockData) {
119-
test(`${file.relativePath} - ${block.description}`, async (t) => {
119+
test(`${file.relativePath} - ${block.description}`, async () => {
120120
// Run rescript-editor-analysis and capture output
121121
const analysisOutput = await new Promise((resolve, reject) => {
122122
const analysisCmd = spawn(
@@ -153,7 +153,14 @@ resFiles.forEach((file) => {
153153
});
154154
});
155155

156-
t.assert.snapshot(analysisOutput.stdout);
156+
// Construct snapshot path
157+
const snapshotDir = path.join(testFilesDir, "__snapshots__");
158+
await fs.mkdir(snapshotDir, { recursive: true }); // Ensure snapshot dir exists
159+
const snapshotFileName = `${file.relativePath}_${block.description.replace(/\\s+/g, "_")}.snap`;
160+
const snapshotPath = path.join(snapshotDir, snapshotFileName);
161+
162+
// Use Vitest's expect().toMatchFileSnapshot()
163+
await expect(analysisOutput.stdout).toMatchFileSnapshot(snapshotPath);
157164
});
158165
}
159166
});

tests/analysis_new_tests/tests/test.js.snapshot

-19
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
Complete /Users/nojaf/Projects/rescript/tests/analysis_new_tests/tests/test_files/.build/RecordFieldCompletions_2.res 1:45
2+
Found Completable: Cexpression
3+
4+
1 + // Record field completion in nested record, another level
5+
2 + let x = TestTypeDefs.nestedTestRecord.nested.
6+
------------------------------------
7+
3 + // ^com
8+
4 +
9+
10+
[{
11+
"label": "name",
12+
"kind": 5,
13+
"tags": [],
14+
"detail": "string",
15+
"documentation": {"kind": "markdown", "value": "```rescript\nname: string\n```\n\n```rescript\ntype \\\"nestedTestRecord.nested\" = {\n name: string,\n oneMoreLevel: {here: bool},\n}\n```"}
16+
}, {
17+
"label": "oneMoreLevel",
18+
"kind": 5,
19+
"tags": [],
20+
"detail": "\\\"nestedTestRecord.nested.oneMoreLevel\"",
21+
"documentation": {"kind": "markdown", "value": "```rescript\noneMoreLevel: \\\"nestedTestRecord.nested.oneMoreLevel\"\n```\n\n```rescript\ntype \\\"nestedTestRecord.nested\" = {\n name: string,\n oneMoreLevel: {here: bool},\n}\n```"}
22+
}]
23+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
Complete /Users/nojaf/Projects/rescript/tests/analysis_new_tests/tests/test_files/.build/RecordFieldCompletions_1.res 1:38
2+
Found Completable: Cexpression
3+
4+
1 + // Record field completion in nested record
5+
2 + let x = TestTypeDefs.nestedTestRecord.
6+
-----------------------------
7+
3 + // ^com
8+
4 +
9+
10+
[{
11+
"label": "test",
12+
"kind": 5,
13+
"tags": [],
14+
"detail": "bool",
15+
"documentation": {"kind": "markdown", "value": "```rescript\ntest: bool\n```\n\n```rescript\ntype nestedTestRecord = {\n test: bool,\n nested: {name: string, oneMoreLevel: {here: bool}},\n}\n```"}
16+
}, {
17+
"label": "nested",
18+
"kind": 5,
19+
"tags": [],
20+
"detail": "\\\"nestedTestRecord.nested\"",
21+
"documentation": {"kind": "markdown", "value": "```rescript\nnested: \\\"nestedTestRecord.nested\"\n```\n\n```rescript\ntype nestedTestRecord = {\n test: bool,\n nested: {name: string, oneMoreLevel: {here: bool}},\n}\n```"}
22+
}]
23+

0 commit comments

Comments
 (0)