diff --git a/CHANGELOG.md b/CHANGELOG.md index fa2bfa774c..7c48f5c1e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/analysis/src/CompletionBackEnd.ml b/analysis/src/CompletionBackEnd.ml index bbd61eac48..13590fc90d 100644 --- a/analysis/src/CompletionBackEnd.ml +++ b/analysis/src/CompletionBackEnd.ml @@ -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 = @@ -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. @@ -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) | CPApply (cp, labels) -> ( if Debug.verbose () then print_endline "[ctx_path]--> CPApply"; match diff --git a/analysis/src/ProcessCmt.ml b/analysis/src/ProcessCmt.ml index 96601f6e3b..4852f1bf1c 100644 --- a/analysis/src/ProcessCmt.ml +++ b/analysis/src/ProcessCmt.ml @@ -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. *) + let in_toplevel_exception = ref false in + let module Iter = TypedtreeIter.MakeIterator (struct + include TypedtreeIter.DefaultIteratorArgument + + let enter_structure_item (item : Typedtree.structure_item) = + (match item.str_desc with + | Tstr_exception _ -> in_toplevel_exception := true + | _ -> ()); + () + + 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 + | _ -> ()); + () + + 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 [] diff --git a/tests/analysis_tests/tests/src/Throw.res b/tests/analysis_tests/tests/src/Throw.res new file mode 100644 index 0000000000..3876f82e5c --- /dev/null +++ b/tests/analysis_tests/tests/src/Throw.res @@ -0,0 +1,5 @@ +exception MyCustomThingToThrow(string) +exception NoArgsToThrow + +// let x = () => thro +// ^com \ No newline at end of file diff --git a/tests/analysis_tests/tests/src/expected/Completion.res.txt b/tests/analysis_tests/tests/src/expected/Completion.res.txt index 0e525703b2..3182b8c445 100644 --- a/tests/analysis_tests/tests/src/expected/Completion.res.txt +++ b/tests/analysis_tests/tests/src/expected/Completion.res.txt @@ -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, diff --git a/tests/analysis_tests/tests/src/expected/CompletionJsxProps.res.txt b/tests/analysis_tests/tests/src/expected/CompletionJsxProps.res.txt index 686f4f5ec9..bcd1aca5f6 100644 --- a/tests/analysis_tests/tests/src/expected/CompletionJsxProps.res.txt +++ b/tests/analysis_tests/tests/src/expected/CompletionJsxProps.res.txt @@ -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, diff --git a/tests/analysis_tests/tests/src/expected/Throw.res.txt b/tests/analysis_tests/tests/src/expected/Throw.res.txt new file mode 100644 index 0000000000..443bdf9229 --- /dev/null +++ b/tests/analysis_tests/tests/src/expected/Throw.res.txt @@ -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 + }] +