Skip to content

Better snapshot #7409

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

Merged
merged 3 commits into from
Apr 29, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
9 changes: 5 additions & 4 deletions analysis/bin/main.ml
Original file line number Diff line number Diff line change
Expand Up @@ -130,11 +130,12 @@ let main () =
| [_; "completion"; path; line; col; currentFile] ->
printHeaderInfo path line col;
if !Cfg.useRevampedCompletion then
Commands.completionRevamped ~debug ~path
let source = Files.readFile currentFile in
Commands.completionRevamped ~source ~debug ~path
~pos:(int_of_string line, int_of_string col)
~currentFile
else
Commands.completion ~debug ~path
Commands.completion ~debug:true ~path
~pos:(int_of_string line, int_of_string col)
~currentFile
| [_; "completionResolve"; path; modulePath] ->
Expand Down Expand Up @@ -213,11 +214,11 @@ let main () =
(Json.escape (CreateInterface.command ~path ~cmiFile))
| [_; "format"; path] ->
Printf.printf "\"%s\"" (Json.escape (Commands.format ~path))
| [_; "test"; path] -> Commands.test ~path
| [_; "test"; path] -> Commands.test ~path ~debug
| [_; "test_revamped"; path; config_file_path] ->
Packages.overrideConfigFilePath := Some config_file_path;
Cfg.useRevampedCompletion := true;
Commands.test ~path
Commands.test ~path ~debug
| args when List.mem "-h" args || List.mem "--help" args -> prerr_endline help
| _ ->
prerr_endline help;
Expand Down
125 changes: 125 additions & 0 deletions analysis/src/CodeFence.ml
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
(* Define a type for a range with start and finish indices *)
type range = {start: int; finish: int}

(* --- Helper function to find the 0-based line index containing a given 0-based character index --- *)
let get_line_index_from_char_index code char_index =
let lines = String.split_on_char '\n' code in
let rec find_line_idx current_char_idx current_line_num remaining_lines =
match remaining_lines with
| [] ->
max 0 (current_line_num - 1)
(* If char_index is beyond the end, return last line index *)
| line :: tl ->
let line_length = String.length line in
(* Check if char_index is within the current line (including the newline char) *)
if
char_index >= current_char_idx
&& char_index <= current_char_idx + line_length
then current_line_num
else
(* Move to the next line, account for the newline character (+1) *)
find_line_idx
(current_char_idx + line_length + 1)
(current_line_num + 1) tl
in
find_line_idx 0 0 lines

(* --- Helper function to calculate the 0-based character index of the start of a given 0-based line index --- *)
let get_char_index_from_line_index code target_line_index =
let lines = String.split_on_char '\n' code in
let rec calculate_start_index_impl current_char_idx current_line_num
lines_to_process =
if current_line_num >= target_line_index then current_char_idx
else
match lines_to_process with
| [] -> current_char_idx (* Target line index is out of bounds *)
| line :: tl ->
(* Move past the current line and its newline character *)
calculate_start_index_impl
(current_char_idx + String.length line + 1)
(current_line_num + 1) tl
in
calculate_start_index_impl 0 0 lines

(* --- Main formatting function --- *)
let format_code_snippet_cropped code (underline_range : range option)
lines_around_annotation =
let lines = String.split_on_char '\n' code in
let total_lines = List.length lines in
let formatted_output = Buffer.create (String.length code) in
(* Initial capacity *)

(* Determine the central line index for cropping *)
let target_line_index =
match underline_range with
| Some {start; finish = _} -> get_line_index_from_char_index code start
| None -> 0 (* Default to first line if no annotations *)
in

(* Determine the cropping window (0-based line indices) *)
let start_line_index = max 0 (target_line_index - lines_around_annotation) in
let end_line_index =
min (total_lines - 1) (target_line_index + lines_around_annotation)
in

(* Keep track of the global character index corresponding to the start of the *current* line being iterated over *)
let current_char_index = ref 0 in

(* Iterate through all original lines to correctly track current_char_index *)
List.iteri
(fun original_line_idx line ->
let line_length = String.length line in
(* Check if the current original line is within our cropping window *)
if
original_line_idx >= start_line_index
&& original_line_idx <= end_line_index
then (
let original_line_number = original_line_idx + 1 in
(* 1-based for display *)
let line_number_prefix = Printf.sprintf "%d + " original_line_number in
let prefix_length = String.length line_number_prefix in

(* Add the code line *)
Buffer.add_string formatted_output line_number_prefix;
Buffer.add_string formatted_output line;
Buffer.add_char formatted_output '\n';

(* Prepare the annotation line buffer *)
let annotation_line_buffer =
Buffer.create (prefix_length + line_length)
in
Buffer.add_string annotation_line_buffer (String.make prefix_length ' ');

(* Initial padding *)
let has_annotation_on_this_line = ref false in

(* Check each character position within this line for annotations *)
for i = 0 to line_length - 1 do
let global_char_index = !current_char_index + i in
let annotation_char = ref ' ' in
(* Default to space *)

(* Check for underline using Option.iter *)
Option.iter
(fun {start; finish} ->
if global_char_index >= start && global_char_index < finish then (
annotation_char := '-' (* '¯' *);
(* Macron symbol *)
has_annotation_on_this_line := true))
underline_range;

Buffer.add_char annotation_line_buffer !annotation_char
done;

(* Add the annotation line to the main output if needed *)
if !has_annotation_on_this_line then (
Buffer.add_buffer formatted_output annotation_line_buffer;
Buffer.add_char formatted_output '\n'));

(* Update the global character index to the start of the next line *)
(* This happens regardless of whether the line was in the cropped window *)
current_char_index := !current_char_index + line_length + 1
(* +1 for the newline *))
lines;

Buffer.contents formatted_output
16 changes: 10 additions & 6 deletions analysis/src/Commands.ml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
let completion ~debug ~path ~pos ~currentFile =
let completion ~(debug : bool) ~path ~pos ~currentFile =
let completions =
match
Completions.getCompletions ~debug ~path ~pos ~currentFile ~forHover:false
Completions.getCompletions debug ~path ~pos ~currentFile ~forHover:false
with
| None -> []
| Some (completions, full, _) ->
Expand All @@ -11,9 +11,11 @@ let completion ~debug ~path ~pos ~currentFile =
in
completions |> Protocol.array |> print_endline

let completionRevamped ~debug ~path ~pos ~currentFile =
let completionRevamped ?(source = None) ~debug ~path ~pos ~currentFile =
let completions =
match Completions.getCompletionsRevamped ~debug ~path ~pos ~currentFile with
match
Completions.getCompletionsRevamped ~source ~debug ~path ~pos ~currentFile
with
| None -> []
| Some (completions, full, _) ->
completions
Expand Down Expand Up @@ -313,7 +315,7 @@ let format ~path =
let diagnosticSyntax ~path =
print_endline (Diagnostics.document_syntax ~path |> Protocol.array)

let test ~path =
let test ~path ~debug =
Uri.stripPath := true;
match Files.readFile path with
| None -> assert false
Expand Down Expand Up @@ -383,7 +385,9 @@ let test ~path =
^ string_of_int col);
let currentFile = createCurrentFile () in
if !Cfg.useRevampedCompletion then
completionRevamped ~debug:true ~path ~pos:(line, col) ~currentFile
let source = Files.readFile currentFile in
completionRevamped ~source ~debug ~path ~pos:(line, col)
~currentFile
else completion ~debug:true ~path ~pos:(line, col) ~currentFile;
Sys.remove currentFile
| "cre" ->
Expand Down
30 changes: 27 additions & 3 deletions analysis/src/Completions.ml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
let getCompletions ~debug ~path ~pos ~currentFile ~forHover =
let getCompletions (debug : bool) ~path ~pos ~currentFile ~forHover =
let textOpt = Files.readFile currentFile in
match textOpt with
| None | Some "" -> None
Expand All @@ -21,7 +21,7 @@ let getCompletions ~debug ~path ~pos ~currentFile ~forHover =
in
Some (completables, full, scope)))

let getCompletionsRevamped ~debug ~path ~pos ~currentFile =
let getCompletionsRevamped ?(source = None) ~debug ~path ~pos ~currentFile =
let textOpt = Files.readFile currentFile in
match textOpt with
| None | Some "" -> None
Expand All @@ -30,8 +30,32 @@ let getCompletionsRevamped ~debug ~path ~pos ~currentFile =
CompletionFrontEndRevamped.completionWithParser ~debug ~path
~posCursor:pos ~currentFile ~text
with
| None -> None
| None ->
source
|> Option.iter (fun _ ->
print_endline "Completion Frontend did not return completable");
None
| Some (completable, scope) -> (
let _ =
match source with
| Some text -> (
match SharedTypes.CompletableRevamped.try_loc completable with
| Some loc ->
let range =
CodeFence.
{
start = loc.Location.loc_start.pos_cnum;
finish = loc.Warnings.loc_end.pos_cnum;
}
in
Printf.printf "Found Completable: %s\n\n"
(SharedTypes.CompletableRevamped.toString completable);
CodeFence.format_code_snippet_cropped text (Some range) 3
|> print_endline
| None -> ())
| None -> ()
in

(* Only perform expensive ast operations if there are completables *)
match Cmt.loadFullCmtFromPath ~path with
| None -> None
Expand Down
2 changes: 1 addition & 1 deletion analysis/src/Hover.ml
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ let hoverWithExpandedTypes ~file ~package ~supportsMarkdownLinks typ =
makes it (most often) work with unsaved content. *)
let getHoverViaCompletions ~debug ~path ~pos ~currentFile ~forHover
~supportsMarkdownLinks =
match Completions.getCompletions ~debug ~path ~pos ~currentFile ~forHover with
match Completions.getCompletions debug ~path ~pos ~currentFile ~forHover with
| None -> None
| Some (completions, ({file; package} as full), scope) -> (
let rawOpens = Scope.getRawOpens scope in
Expand Down
15 changes: 15 additions & 0 deletions analysis/src/SharedTypes.ml
Original file line number Diff line number Diff line change
Expand Up @@ -793,6 +793,21 @@ module CompletableRevamped = struct
| CextensionNode of string
| Cdecorator of string
| CdecoratorPayload of decoratorPayload

let toString (t : t) =
match t with
| Cexpression _ -> "Cexpression"
| Cpattern _ -> "Cpattern"
| Cnone -> "Cnone"
| CextensionNode _ -> "CextensionNode"
| Cdecorator _ -> "Cdecorator"
| CdecoratorPayload _ -> "CdecoratorPayload"

let try_loc (t : t) =
match t with
| Cexpression {typeLoc; _} -> Some typeLoc
| Cpattern {typeLoc; _} -> Some typeLoc
| _ -> None
end

module ScopeTypes = struct
Expand Down
7 changes: 5 additions & 2 deletions tests/analysis_new_tests/tests/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,14 @@
"scripts": {
"build": "rescript",
"clean": "rescript clean -with-deps",
"test": "yarn build && node test.js",
"test:update": "node --test-update-snapshots test.js"
"test": "yarn build && vitest run test.js",
"test:update": "vitest run -u test.js"
},
"dependencies": {
"@rescript/react": "link:../../dependencies/rescript-react",
"rescript": "workspace:^"
},
"devDependencies": {
"vitest": "3.1.2"
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { test } from "node:test";
import { test, expect } from "vitest";
import fs from "node:fs/promises";
import path from "node:path";
import { glob } from "glob";
Expand Down Expand Up @@ -116,7 +116,7 @@ await Promise.all(
resFiles.forEach((file) => {
const blockData = testBlocksPerFile.get(file.relativePath);
for (const block of blockData) {
test(`${file.relativePath} - ${block.description}`, async (t) => {
test(`${file.relativePath} - ${block.description}`, async () => {
// Run rescript-editor-analysis and capture output
const analysisOutput = await new Promise((resolve, reject) => {
const analysisCmd = spawn(
Expand Down Expand Up @@ -153,7 +153,14 @@ resFiles.forEach((file) => {
});
});

t.assert.snapshot(analysisOutput.stdout);
// Construct snapshot path
const snapshotDir = path.join(testFilesDir, "__snapshots__");
await fs.mkdir(snapshotDir, { recursive: true }); // Ensure snapshot dir exists
const snapshotFileName = `${file.relativePath}_${block.description.replace(/\\s+/g, "_")}.snap`;
const snapshotPath = path.join(snapshotDir, snapshotFileName);

// Use Vitest's expect().toMatchFileSnapshot()
await expect(analysisOutput.stdout).toMatchFileSnapshot(snapshotPath);
});
}
});
19 changes: 0 additions & 19 deletions tests/analysis_new_tests/tests/test.js.snapshot

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
Complete /Users/nojaf/Projects/rescript/tests/analysis_new_tests/tests/test_files/.build/RecordFieldCompletions_2.res 1:45
Found Completable: Cexpression

1 + // Record field completion in nested record, another level
2 + let x = TestTypeDefs.nestedTestRecord.nested.
------------------------------------
3 + // ^com
4 +

[{
"label": "name",
"kind": 5,
"tags": [],
"detail": "string",
"documentation": {"kind": "markdown", "value": "```rescript\nname: string\n```\n\n```rescript\ntype \\\"nestedTestRecord.nested\" = {\n name: string,\n oneMoreLevel: {here: bool},\n}\n```"}
}, {
"label": "oneMoreLevel",
"kind": 5,
"tags": [],
"detail": "\\\"nestedTestRecord.nested.oneMoreLevel\"",
"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```"}
}]

Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
Complete /Users/nojaf/Projects/rescript/tests/analysis_new_tests/tests/test_files/.build/RecordFieldCompletions_1.res 1:38
Found Completable: Cexpression

1 + // Record field completion in nested record
2 + let x = TestTypeDefs.nestedTestRecord.
-----------------------------
3 + // ^com
4 +

[{
"label": "test",
"kind": 5,
"tags": [],
"detail": "bool",
"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```"}
}, {
"label": "nested",
"kind": 5,
"tags": [],
"detail": "\\\"nestedTestRecord.nested\"",
"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```"}
}]

Loading