Skip to content
Open
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
- Add (dev-)dependencies to build schema. https://github.com/rescript-lang/rescript/pull/7892
- Dedicated error for dict literal spreads. https://github.com/rescript-lang/rescript/pull/7901
- Dedicated error message for when mixing up `:` and `=` in various positions. https://github.com/rescript-lang/rescript/pull/7900
- Add completions for `throw`. https://github.com/rescript-lang/rescript/pull/7905

#### :house: Internal

Expand Down
39 changes: 37 additions & 2 deletions analysis/src/CompletionBackEnd.ml
Original file line number Diff line number Diff line change
Expand Up @@ -756,6 +756,38 @@ let getCompletionsForPath ~debug ~opens ~full ~pos ~exact ~scope
findAllCompletions ~env ~prefix ~exact ~namesUsed ~completionContext
| None -> []))

let completionsForThrow ~(env : QueryEnv.t) ~full =
let exn_typ = Predef.type_exn in
let names_from_cmt =
let moduleName = env.file.moduleName in
match Hashtbl.find_opt full.package.pathsForModule moduleName with
| None -> []
| Some paths ->
let uri = getUri paths in
let cmt_path = getCmtPath ~uri paths in
ProcessCmt.exceptionsForCmt ~cmt:cmt_path
in
let completions_from_cmt =
names_from_cmt
|> List.map (fun (name, hasArgs) ->
let insertText =
if hasArgs then Printf.sprintf "throw(%s($0))" name
else Printf.sprintf "throw(%s)" name
in
Completion.create
(Printf.sprintf "throw(%s)" name)
~env ~kind:(Completion.Value exn_typ) ~includesSnippets:true
~insertText ~filterText:"throw")
in
Completion.create "JsError.throwWithMessage" ~env
~kind:(Completion.Value exn_typ) ~includesSnippets:true
~detail:"Throw a JavaScript error, example: `throw new Error(str)`"
~insertText:"JsError.throwWithMessage(\"$0\")"
:: Completion.create "JsExn.throw" ~env ~kind:(Completion.Value exn_typ)
~includesSnippets:true ~insertText:"JsExn.throw($0)"
~detail:"Throw any JavaScript value, example: `throw 100`"
:: completions_from_cmt

(** Completions intended for piping, from a completion path. *)
let completionsForPipeFromCompletionPath ~envCompletionIsMadeFrom ~opens ~pos
~scope ~debug ~prefix ~env ~rawOpens ~full completionPath =
Expand Down Expand Up @@ -1010,7 +1042,7 @@ and getCompletionsForContextPath ~debug ~full ~opens ~rawOpens ~pos ~env ~exact
| Some (Tpromise (env, typ), _env) ->
[Completion.create "dummy" ~env ~kind:(Completion.Value typ)]
| _ -> [])
| CPId {path; completionContext; loc} ->
| CPId {path; completionContext; loc} -> (
if Debug.verbose () then print_endline "[ctx_path]--> CPId";
(* Looks up the type of an identifier.

Expand Down Expand Up @@ -1048,7 +1080,10 @@ and getCompletionsForContextPath ~debug ~full ~opens ~rawOpens ~pos ~env ~exact
| _ -> byPath
else byPath
in
result
match (result, path) with
| [], [prefix] when Utils.startsWith "throw" prefix ->
completionsForThrow ~env ~full
| _ -> result)
Comment on lines +1083 to +1086
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should probably be done in a "safer" way that matches by type and not by name. We'd need to check if this thing actually resolves to Pervasives.throw.

Also, why Utils.startsWith? Is it not supposed to be an exact match?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nojaf this has not been answered I believe.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'd need to check if this thing actually resolves to Pervasives.throw.

If you only have thr you can't do that.

Also, why Utils.startsWith? Is it not supposed to be an exact match?

As mentioned, I want the completions to show up while I'm typing towards the word throw. Only having it once the full word is typed I find unintuitive.

| CPApply (cp, labels) -> (
if Debug.verbose () then print_endline "[ctx_path]--> CPApply";
match
Expand Down
74 changes: 74 additions & 0 deletions analysis/src/ProcessCmt.ml
Original file line number Diff line number Diff line change
Expand Up @@ -793,3 +793,77 @@ let fileForModule moduleName ~package =
| None ->
Log.log ("No path for module " ^ moduleName);
None

(* Collect top-level exception constructors from typedtree/CMT file. *)
let exceptionsForCmt ~cmt : (string * bool) list =
match Shared.tryReadCmt cmt with
| None -> []
| Some infos ->
let by_name : (string, bool) Hashtbl.t = Hashtbl.create 16 in
let add_ext (ext : Typedtree.extension_constructor) : unit =
let name = ext.ext_name.txt in
let hasArgs =
match ext.ext_kind with
| Text_decl (Cstr_tuple args, _ret) -> args <> []
| Text_decl (Cstr_record fields, _ret) -> fields <> []
| Text_rebind _ -> true
in
let prev =
match Hashtbl.find_opt by_name name with
| Some b -> b
| None -> false
in
Hashtbl.replace by_name name (prev || hasArgs)
in
(* Only collect top-level exception declarations (Tstr_exception/Tsig_exception).
Avoid picking up exceptions from Texp_letexception by tracking context. *)
Comment on lines +818 to +819
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In theory I guess the most robust approach would be to collect and use whatever defined exceptions are in scope at the current position. Collecting at the top level is good and simple, but you could also define them in sub modules, and so on.

It might be worth exploring making exceptions part of the tracked scope instead, and track them just like we track types and values today when doing completion. Then we could just extend the current mechanism a bit, and hopefully get the correct scope tracking for free.

let in_toplevel_exception = ref false in
let module Iter = TypedtreeIter.MakeIterator (struct
include TypedtreeIter.DefaultIteratorArgument
Comment on lines +821 to +822
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we use the functor based approach elsewhere in the code base? Or is there no way for the typed tree to just define a mapper like we do for the AST mapper? I don't remember how this works exactly.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this was weird to me as well, it is a departure from what we have on the untyped side, but this seems to be the mapper we have?


let enter_structure_item (item : Typedtree.structure_item) =
(match item.str_desc with
| Tstr_exception _ -> in_toplevel_exception := true
| _ -> ());
()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is there an extra unit here...?


let leave_structure_item (_ : Typedtree.structure_item) =
in_toplevel_exception := false

let enter_signature_item (item : Typedtree.signature_item) =
(match item.sig_desc with
| Tsig_exception _ -> in_toplevel_exception := true
| _ -> ());
()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same, extra unit.


let leave_signature_item (_ : Typedtree.signature_item) =
in_toplevel_exception := false

let enter_extension_constructor (ext : Typedtree.extension_constructor) =
if !in_toplevel_exception then add_ext ext
end) in
let () =
match infos.cmt_annots with
| Cmt_format.Implementation s -> Iter.iter_structure s
| Interface s -> Iter.iter_signature s
| Partial_implementation parts ->
Array.iter
(function
| Cmt_format.Partial_structure s -> Iter.iter_structure s
| Partial_structure_item si -> Iter.iter_structure_item si
| Partial_signature s -> Iter.iter_signature s
| Partial_signature_item si -> Iter.iter_signature_item si
| _ -> ())
parts
| Partial_interface parts ->
Array.iter
(function
| Cmt_format.Partial_structure s -> Iter.iter_structure s
| Partial_structure_item si -> Iter.iter_structure_item si
| Partial_signature s -> Iter.iter_signature s
| Partial_signature_item si -> Iter.iter_signature_item si
| _ -> ())
parts
| _ -> ()
in
Hashtbl.fold (fun name hasArgs acc -> (name, hasArgs) :: acc) by_name []
5 changes: 5 additions & 0 deletions tests/analysis_tests/tests/src/Throw.res
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
exception MyCustomThingToThrow(string)
exception NoArgsToThrow

// let x = () => thro
// ^com
10 changes: 10 additions & 0 deletions tests/analysis_tests/tests/src/expected/Completion.res.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2147,6 +2147,16 @@ Path T
"modulePath": "TableclothMap",
"filePath": "src/Completion.res"
}
}, {
"label": "Throw",
"kind": 9,
"tags": [],
"detail": "module Throw",
"documentation": null,
"data": {
"modulePath": "Throw",
"filePath": "src/Completion.res"
}
}, {
"label": "TypeArgCtx",
"kind": 9,
Expand Down
10 changes: 10 additions & 0 deletions tests/analysis_tests/tests/src/expected/CompletionJsxProps.res.txt
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,16 @@ Path CompletionSupport.TestComponent.make
"modulePath": "TableclothMap",
"filePath": "src/CompletionJsxProps.res"
}
}, {
"label": "Throw",
"kind": 9,
"tags": [],
"detail": "module Throw",
"documentation": null,
"data": {
"modulePath": "Throw",
"filePath": "src/CompletionJsxProps.res"
}
}, {
"label": "TypeArgCtx",
"kind": 9,
Expand Down
45 changes: 45 additions & 0 deletions tests/analysis_tests/tests/src/expected/Throw.res.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
Complete src/Throw.res 3:21
posCursor:[3:21] posNoWhite:[3:20] Found expr:[3:11->3:21]
posCursor:[3:21] posNoWhite:[3:20] Found expr:[3:17->3:21]
Pexp_ident thro:[3:17->3:21]
Completable: Cpath Value[thro]
Package opens Stdlib.place holder Pervasives.JsxModules.place holder
Resolved opens 1 Stdlib
ContextPath Value[thro]
Path thro
[{
"label": "JsError.throwWithMessage",
"kind": 12,
"tags": [],
"detail": "Throw a JavaScript error, example: `throw new Error(str)`",
"documentation": null,
"insertText": "JsError.throwWithMessage(\"$0\")",
"insertTextFormat": 2
}, {
"label": "JsExn.throw",
"kind": 12,
"tags": [],
"detail": "Throw any JavaScript value, example: `throw 100`",
"documentation": null,
"insertText": "JsExn.throw($0)",
"insertTextFormat": 2
}, {
"label": "throw(MyCustomThingToThrow)",
"kind": 12,
"tags": [],
"detail": "exn",
"documentation": null,
"filterText": "throw",
"insertText": "throw(MyCustomThingToThrow($0))",
"insertTextFormat": 2
}, {
"label": "throw(NoArgsToThrow)",
"kind": 12,
"tags": [],
"detail": "exn",
"documentation": null,
"filterText": "throw",
"insertText": "throw(NoArgsToThrow)",
"insertTextFormat": 2
}]

Loading