diff --git a/CHANGELOG.md b/CHANGELOG.md index f8e7301d9b..ce253bd8d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,7 @@ #### :rocket: New Feature - Reanalyze: add scoped `@@live`/`@@dead` annotations for marking module/file sections as live or dead. https://github.com/rescript-lang/rescript/pull/8197 +- Add completions for `throw`. https://github.com/rescript-lang/rescript/pull/7905 #### :bug: Bug fix diff --git a/analysis/src/CompletionBackEnd.ml b/analysis/src/CompletionBackEnd.ml index a24b4315fb..1da326c2ce 100644 --- a/analysis/src/CompletionBackEnd.ml +++ b/analysis/src/CompletionBackEnd.ml @@ -756,6 +756,50 @@ let getCompletionsForPath ~debug ~opens ~full ~pos ~exact ~scope findAllCompletions ~env ~prefix ~exact ~namesUsed ~completionContext | None -> [])) +let getExceptionNamesFromCmt ~(env : QueryEnv.t) ~full = + 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 + +let completionsForThrowArg ~(env : QueryEnv.t) ~full = + let exn_typ = Predef.type_exn in + let names_from_cmt = getExceptionNamesFromCmt ~env ~full in + names_from_cmt + |> List.map (fun (name, hasArgs) -> + let insertText = + if hasArgs then Printf.sprintf "%s($0)" name else name + in + Completion.create name ~env ~kind:(Completion.Value exn_typ) + ~includesSnippets:hasArgs ~insertText) + +let completionsForThrow ~(env : QueryEnv.t) ~full = + let exn_typ = Predef.type_exn in + let names_from_cmt = getExceptionNamesFromCmt ~env ~full 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 +1054,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 +1092,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 @@ -2283,105 +2330,125 @@ let rec processCompletable ~debug ~full ~scope ~env ~pos ~forHover completable = fallbackOrEmpty ~items ()) | None -> fallbackOrEmpty ()) | Cexpression {contextPath; prefix; nested} -> ( - let isAmbigiousRecordBodyOrJsxWrap = - match (contextPath, nested) with - | CJsxPropValue _, [NRecordBody _] -> true + (* Special case: completing argument to throw() should return exception constructors *) + let isThrowArg = + match contextPath with + | CArgument + { + functionContextPath = + CPId {path = ["throw"]; completionContext = Value}; + argumentLabel = Unlabelled _; + } -> + true | _ -> false in - if Debug.verbose () then - (* This happens in this scenario: `}` - Here, we don't know whether `{}` is just wraps for the type of - `someProp`, or if it's a record body where we want to complete - for the fields in the record. We need to look up what the type is - first before deciding what completions to show. So we do that here.*) - if isAmbigiousRecordBodyOrJsxWrap then - print_endline - "[process_completable]--> Cexpression special case: JSX prop value \ - that might be record body or JSX wrap" - else print_endline "[process_completable]--> Cexpression"; - (* Completions for local things like variables in scope, modules in the + if isThrowArg then + completionsForThrowArg ~env ~full + |> List.filter (fun (c : Completion.t) -> + prefix = "" || Utils.startsWith c.name prefix) + else + let isAmbigiousRecordBodyOrJsxWrap = + match (contextPath, nested) with + | CJsxPropValue _, [NRecordBody _] -> true + | _ -> false + in + if Debug.verbose () then + (* This happens in this scenario: `}` + Here, we don't know whether `{}` is just wraps for the type of + `someProp`, or if it's a record body where we want to complete + for the fields in the record. We need to look up what the type is + first before deciding what completions to show. So we do that here.*) + if isAmbigiousRecordBodyOrJsxWrap then + print_endline + "[process_completable]--> Cexpression special case: JSX prop value \ + that might be record body or JSX wrap" + else print_endline "[process_completable]--> Cexpression"; + (* Completions for local things like variables in scope, modules in the project, etc. We only add completions when there's a prefix of some sort we can filter on, since we know we're in some sort of context, and therefore don't want to overwhelm the user with completion items. *) - let regularCompletions = - if prefix = "" then [] - else - prefix - |> getComplementaryCompletionsForTypedValue ~opens ~allFiles ~env ~scope - in - match - contextPath - |> getCompletionsForContextPath ~debug ~full ~opens ~rawOpens ~pos ~env - ~exact:true ~scope - |> completionsGetCompletionType ~full - with - | None -> - if Debug.verbose () then - print_endline - "[process_completable]--> could not get completions for context path"; - regularCompletions - | Some (typ, env) -> ( - match typ |> TypeUtils.resolveNested ~env ~full ~nested with + let regularCompletions = + if prefix = "" then [] + else + prefix + |> getComplementaryCompletionsForTypedValue ~opens ~allFiles ~env + ~scope + in + match + contextPath + |> getCompletionsForContextPath ~debug ~full ~opens ~rawOpens ~pos ~env + ~exact:true ~scope + |> completionsGetCompletionType ~full + with | None -> if Debug.verbose () then print_endline - "[process_completable]--> could not resolve nested expression path"; - if isAmbigiousRecordBodyOrJsxWrap then ( + "[process_completable]--> could not get completions for context \ + path"; + regularCompletions + | Some (typ, env) -> ( + match typ |> TypeUtils.resolveNested ~env ~full ~nested with + | None -> if Debug.verbose () then print_endline - "[process_completable]--> case is ambigious Jsx prop vs record \ - body case, complete also for the JSX prop value directly"; - let itemsForRawJsxPropValue = - typ - |> completeTypedValue ~rawOpens ~mode:Expression ~full ~prefix - ~completionContext:None - in - itemsForRawJsxPropValue @ regularCompletions) - else regularCompletions - | Some (typ, _env, completionContext, typeArgContext) -> ( - if Debug.verbose () then - print_endline - "[process_completable]--> found type in nested expression \ - completion"; - (* Wrap the insert text in braces when we're completing the root of a + "[process_completable]--> could not resolve nested expression \ + path"; + if isAmbigiousRecordBodyOrJsxWrap then ( + if Debug.verbose () then + print_endline + "[process_completable]--> case is ambigious Jsx prop vs record \ + body case, complete also for the JSX prop value directly"; + let itemsForRawJsxPropValue = + typ + |> completeTypedValue ~rawOpens ~mode:Expression ~full ~prefix + ~completionContext:None + in + itemsForRawJsxPropValue @ regularCompletions) + else regularCompletions + | Some (typ, _env, completionContext, typeArgContext) -> ( + if Debug.verbose () then + print_endline + "[process_completable]--> found type in nested expression \ + completion"; + (* Wrap the insert text in braces when we're completing the root of a JSX prop value. *) - let wrapInsertTextInBraces = - if List.length nested > 0 then false - else - match contextPath with - | CJsxPropValue _ -> true - | _ -> false - in - let items = - typ - |> completeTypedValue ?typeArgContext ~rawOpens ~mode:Expression ~full - ~prefix ~completionContext - |> List.map (fun (c : Completion.t) -> - if wrapInsertTextInBraces then - { - c with - insertText = - (match c.insertText with - | None -> None - | Some text -> Some ("{" ^ text ^ "}")); - } - else c) - in - match (prefix, completionContext) with - | "", _ -> items - | _, None -> + let wrapInsertTextInBraces = + if List.length nested > 0 then false + else + match contextPath with + | CJsxPropValue _ -> true + | _ -> false + in let items = - if List.length regularCompletions > 0 then - (* The client will occasionally sort the list of completions alphabetically, disregarding the order + typ + |> completeTypedValue ?typeArgContext ~rawOpens ~mode:Expression + ~full ~prefix ~completionContext + |> List.map (fun (c : Completion.t) -> + if wrapInsertTextInBraces then + { + c with + insertText = + (match c.insertText with + | None -> None + | Some text -> Some ("{" ^ text ^ "}")); + } + else c) + in + match (prefix, completionContext) with + | "", _ -> items + | _, None -> + let items = + if List.length regularCompletions > 0 then + (* The client will occasionally sort the list of completions alphabetically, disregarding the order in which we send it. This fixes that by providing a sort text making the typed completions guaranteed to end up on top. *) - items - |> List.map (fun (c : Completion.t) -> - {c with sortText = Some ("A" ^ " " ^ c.name)}) - else items - in - items @ regularCompletions - | _ -> items))) + items + |> List.map (fun (c : Completion.t) -> + {c with sortText = Some ("A" ^ " " ^ c.name)}) + else items + in + items @ regularCompletions + | _ -> items))) | CexhaustiveSwitch {contextPath; exprLoc} -> let range = Utils.rangeOfLoc exprLoc in let printFailwithStr num = "${" ^ string_of_int num ^ ":%todo}" in diff --git a/analysis/src/ProcessCmt.ml b/analysis/src/ProcessCmt.ml index 96601f6e3b..692569eb76 100644 --- a/analysis/src/ProcessCmt.ml +++ b/analysis/src/ProcessCmt.ml @@ -793,3 +793,75 @@ 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..1a27406f2e --- /dev/null +++ b/tests/analysis_tests/tests/src/Throw.res @@ -0,0 +1,11 @@ +exception MyCustomThingToThrow(string) +exception NoArgsToThrow + +// let x = () => thro +// ^com + +// let y = () => throw(MyCu) +// ^com + +// let z = () => throw() +// ^com diff --git a/tests/analysis_tests/tests/src/expected/Completion.res.txt b/tests/analysis_tests/tests/src/expected/Completion.res.txt index df30210ef2..b7cc4b1ae4 100644 --- a/tests/analysis_tests/tests/src/expected/Completion.res.txt +++ b/tests/analysis_tests/tests/src/expected/Completion.res.txt @@ -2153,6 +2153,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 af410bf97a..7bca23f80d 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..e54b52e774 --- /dev/null +++ b/tests/analysis_tests/tests/src/expected/Throw.res.txt @@ -0,0 +1,86 @@ +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 + }] + +Complete src/Throw.res 6:27 +posCursor:[6:27] posNoWhite:[6:26] Found expr:[6:11->6:28] +posCursor:[6:27] posNoWhite:[6:26] Found expr:[6:17->6:28] +Pexp_apply ...[6:17->6:22] (...[6:23->6:27]) +Completable: Cexpression CArgument Value[throw]($0)=MyCu +Package opens Stdlib.place holder Pervasives.JsxModules.place holder +Resolved opens 1 Stdlib +[{ + "label": "MyCustomThingToThrow", + "kind": 12, + "tags": [], + "detail": "exn", + "documentation": null, + "insertText": "MyCustomThingToThrow($0)", + "insertTextFormat": 2 + }] + +Complete src/Throw.res 9:23 +posCursor:[9:23] posNoWhite:[9:22] Found expr:[9:11->9:24] +posCursor:[9:23] posNoWhite:[9:22] Found expr:[9:17->9:24] +Pexp_apply ...[9:17->9:22] (...[9:23->9:24]) +Completable: Cexpression CArgument Value[throw]($0) +Package opens Stdlib.place holder Pervasives.JsxModules.place holder +Resolved opens 1 Stdlib +[{ + "label": "MyCustomThingToThrow", + "kind": 12, + "tags": [], + "detail": "exn", + "documentation": null, + "insertText": "MyCustomThingToThrow($0)", + "insertTextFormat": 2 + }, { + "label": "NoArgsToThrow", + "kind": 12, + "tags": [], + "detail": "exn", + "documentation": null, + "insertText": "NoArgsToThrow" + }] +