diff --git a/.vscode/settings.json b/.vscode/settings.json index 11909bf1d..2b65f0986 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -8,6 +8,6 @@ }, "ocaml.sandbox": { "kind": "opam", - "switch": "${workspaceFolder:rescript-vscode}/analysis" + "switch": "4.14.0" } } \ No newline at end of file diff --git a/analysis/src/Commands.ml b/analysis/src/Commands.ml index c46225fa0..d59bb710f 100644 --- a/analysis/src/Commands.ml +++ b/analysis/src/Commands.ml @@ -12,6 +12,20 @@ let completion ~debug ~path ~pos ~currentFile = |> List.map Protocol.stringifyCompletionItem |> Protocol.array) +let completionNew ~debug ~path ~pos ~currentFile = + let completions = + match + Completions.getCompletions2 ~debug ~path ~pos ~currentFile ~forHover:false + with + | None -> [] + | Some (completions, _, _) -> completions + in + print_endline + (completions + |> List.map CompletionBackEnd.completionToItem + |> List.map Protocol.stringifyCompletionItem + |> Protocol.array) + let inlayhint ~path ~pos ~maxLength ~debug = let result = match Hint.inlay ~path ~pos ~maxLength ~debug with @@ -316,6 +330,13 @@ let test ~path = let currentFile = createCurrentFile () in completion ~debug:true ~path ~pos:(line, col) ~currentFile; Sys.remove currentFile + | "co2" -> + print_endline + ("Complete2 " ^ path ^ " " ^ string_of_int line ^ ":" + ^ string_of_int col); + let currentFile = createCurrentFile () in + completionNew ~debug:true ~path ~pos:(line, col) ~currentFile; + Sys.remove currentFile | "dce" -> print_endline ("DCE " ^ path); Reanalyze.RunConfig.runConfig.suppress <- ["src"]; diff --git a/analysis/src/CompletionBackEnd.ml b/analysis/src/CompletionBackEnd.ml index 61acb9d08..58b660d21 100644 --- a/analysis/src/CompletionBackEnd.ml +++ b/analysis/src/CompletionBackEnd.ml @@ -286,7 +286,10 @@ let processLocalValue name loc contextPath ~prefix ~exact ~env Completion.create name ~env ~kind: (match contextPath with - | Some contextPath -> FollowContextPath contextPath + | Some (Scope.Completable contextPath) -> + FollowContextPath (`Completable contextPath) + | Some (Scope.New contextPath) -> + FollowContextPath (`New contextPath) | None -> Value (Ctype.newconstr @@ -628,7 +631,7 @@ let rec completionsGetCompletionType2 ~debug ~full ~opens ~rawOpens ~pos ~scope | {Completion.kind = ObjLabel typ; env} :: _ | {Completion.kind = Field ({typ}, _); env} :: _ -> Some (TypeExpr typ, env) - | {Completion.kind = FollowContextPath ctxPath; env} :: _ -> + | {Completion.kind = FollowContextPath (`Completable ctxPath); env} :: _ -> ctxPath |> getCompletionsForContextPath ~debug ~full ~env ~exact:true ~opens ~rawOpens ~pos ~scope @@ -647,7 +650,7 @@ and completionsGetTypeEnv2 ~debug (completions : Completion.t list) ~full ~opens | {Completion.kind = Value typ; env} :: _ -> Some (typ, env) | {Completion.kind = ObjLabel typ; env} :: _ -> Some (typ, env) | {Completion.kind = Field ({typ}, _); env} :: _ -> Some (typ, env) - | {Completion.kind = FollowContextPath ctxPath; env} :: _ -> + | {Completion.kind = FollowContextPath (`Completable ctxPath); env} :: _ -> ctxPath |> getCompletionsForContextPath ~debug ~full ~opens ~rawOpens ~pos ~env ~exact:true ~scope diff --git a/analysis/src/CompletionBackendNew.ml b/analysis/src/CompletionBackendNew.ml new file mode 100644 index 000000000..772078724 --- /dev/null +++ b/analysis/src/CompletionBackendNew.ml @@ -0,0 +1,1496 @@ +open SharedTypes +open CompletionNewTypes +open CompletionsNewTypesCtxPath + +(* TODO: Unify and clean these up once we have tests *) + +let debugTypeLookups = true + +let getCompletionsForPath = CompletionBackEnd.getCompletionsForPath +let getOpens = CompletionBackEnd.getOpens +let getComplementaryCompletionsForTypedValue = + CompletionBackEnd.getComplementaryCompletionsForTypedValue + +let rec completionsGetCompletionType ~full = function + | {Completion.kind = Value typ; env} :: _ + | {Completion.kind = ObjLabel typ; env} :: _ + | {Completion.kind = Field ({typ}, _); env} :: _ -> + typ + |> TypeUtils.extractType ~env ~package:full.package + |> Option.map (fun typ -> (typ, env)) + | {Completion.kind = Type typ; env} :: _ -> ( + match TypeUtils.extractTypeFromResolvedType typ ~env ~full with + | None -> None + | Some extractedType -> Some (extractedType, env)) + | {Completion.kind = ExtractedType (typ, _); env} :: _ -> Some (typ, env) + | _ -> None + +and completionsGetCompletionTypeX ~full = function + | {Completion.kind = Value typ; env} :: _ + | {Completion.kind = ObjLabel typ; env} :: _ + | {Completion.kind = Field ({typ}, _); env} :: _ -> + typ + |> TypeUtils.extractType ~env ~package:full.package + |> Option.map (fun typ -> (typ, env)) + | {Completion.kind = Type typ; env} :: _ -> ( + match TypeUtils.extractTypeFromResolvedType typ ~env ~full with + | None -> None + | Some extractedType -> Some (extractedType, env)) + | {Completion.kind = ExtractedType (typ, _); env} :: _ -> Some (typ, env) + | _ -> None + +and completionsGetCompletionType2 ~debug ~full ~opens ~rawOpens ~pos ~scope = + function + | {Completion.kind = Value typ; env} :: _ + | {Completion.kind = ObjLabel typ; env} :: _ + | {Completion.kind = Field ({typ}, _); env} :: _ -> + Some (TypeExpr typ, env) + | {Completion.kind = FollowContextPath (`New ctxPath); env} :: _ -> + ctxPath + |> getCompletionsForContextPath ~debug ~full ~env ~exact:true ~opens + ~rawOpens ~pos ~scope + |> completionsGetCompletionType2 ~debug ~full ~opens ~rawOpens ~pos ~scope + | {Completion.kind = Type typ; env} :: _ -> ( + match TypeUtils.extractTypeFromResolvedType typ ~env ~full with + | None -> None + | Some extractedType -> Some (ExtractedType extractedType, env)) + | {Completion.kind = ExtractedType (typ, _); env} :: _ -> + Some (ExtractedType typ, env) + | _ -> None + +and completionsGetTypeEnv ~debug (completions : Completion.t list) ~full ~opens + ~rawOpens ~pos ~scope = + match completions with + | {Completion.kind = Value typ; env} :: _ -> Some (typ, env) + | {Completion.kind = ObjLabel typ; env} :: _ -> Some (typ, env) + | {Completion.kind = Field ({typ}, _); env} :: _ -> Some (typ, env) + | {Completion.kind = FollowContextPath (`New ctxPath); env} :: _ -> + ctxPath + |> getCompletionsForContextPath ~debug ~full ~opens ~rawOpens ~pos ~env + ~exact:true ~scope + |> completionsGetTypeEnv ~debug ~full ~opens ~rawOpens ~pos ~scope + | _ -> None + +and getCompletionsForContextPath ~debug ~full ~opens ~rawOpens ~pos + ~(env : QueryEnv.t) ~exact ~(scope : Scope.t) (contextPath : ctxPath) = + let package = full.package in + match contextPath with + | CString -> + if debugTypeLookups then Printf.printf "CString: returning string\n"; + [ + Completion.create "dummy" ~env + ~kind: + (Completion.Value + (Ctype.newconstr (Path.Pident (Ident.create "string")) [])); + ] + | CBool -> + if debugTypeLookups then Printf.printf "CBool: returning bool\n"; + [ + Completion.create "dummy" ~env + ~kind: + (Completion.Value + (Ctype.newconstr (Path.Pident (Ident.create "bool")) [])); + ] + | CInt -> + if debugTypeLookups then Printf.printf "CInt: returning int\n"; + [ + Completion.create "dummy" ~env + ~kind: + (Completion.Value + (Ctype.newconstr (Path.Pident (Ident.create "int")) [])); + ] + | CFloat -> + if debugTypeLookups then Printf.printf "CFloat: returning float\n"; + [ + Completion.create "dummy" ~env + ~kind: + (Completion.Value + (Ctype.newconstr (Path.Pident (Ident.create "float")) [])); + ] + | CArray None -> + if debugTypeLookups then Printf.printf "CArray: array with no payload\n"; + [ + Completion.create "array" ~env + ~kind: + (Completion.Value + (Ctype.newconstr (Path.Pident (Ident.create "array")) [])); + ] + | CArray (Some cp) -> ( + match + cp + |> getCompletionsForContextPath ~debug ~full ~opens ~rawOpens ~pos ~env + ~exact:true ~scope + |> completionsGetCompletionType ~full + with + | None -> + if debugTypeLookups then + Printf.printf "CArray (with payload): could not look up payload\n"; + [] + | Some (typ, env) -> + if debugTypeLookups then + Printf.printf "CArray (with payload): returning array with payload %s\n" + (TypeUtils.extractedTypeToString typ); + [ + Completion.create "dummy" ~env + ~kind: + (Completion.ExtractedType (Tarray (env, ExtractedType typ), `Type)); + ]) + | COption cp -> ( + match + cp + |> getCompletionsForContextPath ~debug ~full ~opens ~rawOpens ~pos ~env + ~exact:true ~scope + |> completionsGetCompletionType ~full + with + | None -> + if debugTypeLookups then + Printf.printf "COption: could not look up payload\n"; + [] + | Some (typ, env) -> + if debugTypeLookups then + Printf.printf "COption: returning option with payload %s\n" + (TypeUtils.extractedTypeToString typ); + [ + Completion.create "dummy" ~env + ~kind: + (Completion.ExtractedType (Toption (env, ExtractedType typ), `Type)); + ]) + | CAwait cp -> ( + match + cp + |> getCompletionsForContextPath ~debug ~full ~opens ~rawOpens ~pos ~env + ~exact:true ~scope + |> completionsGetCompletionType ~full + with + | Some (Tpromise (env, typ), _env) -> + if debugTypeLookups then + Printf.printf "CAwait: found type to unwrap in promise: %s\n" + (Shared.typeToString typ); + [Completion.create "dummy" ~env ~kind:(Completion.Value typ)] + | Some (typ, _) -> + if debugTypeLookups then + Printf.printf + "CAwait: found something other than a promise at await ctx path: %s\n" + (TypeUtils.extractedTypeToString typ); + [] + | None -> + if debugTypeLookups then + Printf.printf "CAwait: found no type at await ctx path\n"; + []) + | CId (path, completionContext) -> + path + |> getCompletionsForPath ~debug ~package ~opens ~full ~pos ~exact + ~completionContext: + (match completionContext with + | Value -> Value + | Module -> Module + | Field -> Field + | Type -> Type) + ~env ~scope + | CApply {functionCtxPath; args = labels} -> ( + match + functionCtxPath + |> getCompletionsForContextPath ~debug ~full ~opens ~rawOpens ~pos ~env + ~exact:true ~scope + |> completionsGetCompletionType2 ~debug ~full ~opens ~rawOpens ~pos ~scope + with + | Some ((TypeExpr typ | ExtractedType (Tfunction {typ})), env) -> ( + let rec reconstructFunctionType args tRet = + match args with + | [] -> tRet + | (label, tArg) :: rest -> + let restType = reconstructFunctionType rest tRet in + {typ with desc = Tarrow (label, tArg, restType, Cok)} + in + let rec processApply args labels = + match (args, labels) with + | _, [] -> args + | _, label :: (_ :: _ as nextLabels) -> + (* compute the application of the first label, then the next ones *) + let args = processApply args [label] in + processApply args nextLabels + | (Asttypes.Nolabel, _) :: nextArgs, [Asttypes.Nolabel] -> nextArgs + | ((Labelled _, _) as arg) :: nextArgs, [Nolabel] -> + arg :: processApply nextArgs labels + | (Optional _, _) :: nextArgs, [Nolabel] -> processApply nextArgs labels + | ( (((Labelled s1 | Optional s1), _) as arg) :: nextArgs, + [(Labelled s2 | Optional s2)] ) -> + if s1 = s2 then nextArgs else arg :: processApply nextArgs labels + | ((Nolabel, _) as arg) :: nextArgs, [(Labelled _ | Optional _)] -> + arg :: processApply nextArgs labels + | [], [(Nolabel | Labelled _ | Optional _)] -> + (* should not happen, but just ignore extra arguments *) [] + in + match TypeUtils.extractFunctionType ~env ~package typ with + | args, tRet when args <> [] -> + let args = processApply args labels in + let retType = reconstructFunctionType args tRet in + if debugTypeLookups then + Printf.printf "CApply: returning apply return type %s\n" + (Shared.typeToString typ); + [Completion.create "dummy" ~env ~kind:(Completion.Value retType)] + | _ -> + if debugTypeLookups then + Printf.printf "CApply: could not extract function type from %s\n" + (Shared.typeToString typ); + []) + | _ -> + if debugTypeLookups then + Printf.printf "CApply: looked for a function but found something else\n"; + []) + | CRecordFieldAccess {recordCtxPath = CId (path, Module); fieldName} -> + (* M.field *) + path @ [fieldName] + |> getCompletionsForPath ~debug ~package ~opens ~full ~pos ~exact + ~completionContext:Field ~env ~scope + | CRecordFieldAccess {recordCtxPath; fieldName} -> ( + let completionsForCtxPath = + recordCtxPath + |> getCompletionsForContextPath ~debug ~full ~opens ~rawOpens ~pos ~env + ~exact:true ~scope + in + let extracted = + match + completionsForCtxPath + |> completionsGetCompletionType2 ~debug ~full ~opens ~rawOpens ~pos + ~scope + with + | Some (TypeExpr typ, env) -> ( + match typ |> TypeUtils.extractRecordType ~env ~package with + | Some (env, fields, typDecl) -> + Some + ( env, + fields, + typDecl.item.decl |> Shared.declToString typDecl.name.txt ) + | None -> None) + | Some (ExtractedType typ, env) -> ( + match typ with + | Trecord {fields} -> + Some (env, fields, typ |> TypeUtils.extractedTypeToString) + | _ -> None) + | None -> None + in + match extracted with + | None -> + if debugTypeLookups then + Printf.printf "CRecordFieldAccess: found type is not a record\n"; + [] + | Some (env, fields, recordAsString) -> + if debugTypeLookups then + Printf.printf + "CRecordFieldAccess: found record and now filtering fields\n"; + fields + |> Utils.filterMap (fun field -> + if Utils.checkName field.fname.txt ~prefix:fieldName ~exact then + Some + (Completion.create field.fname.txt ~env + ?deprecated:field.deprecated ~docstring:field.docstring + ~kind:(Completion.Field (field, recordAsString))) + else None)) + | CObj {objectCtxPath; propertyName} -> ( + (* TODO: Also needs to support ExtractedType *) + match + objectCtxPath + |> getCompletionsForContextPath ~debug ~full ~opens ~rawOpens ~pos ~env + ~exact:true ~scope + |> completionsGetTypeEnv ~debug ~full ~opens ~rawOpens ~pos ~scope + with + | Some (typ, env) -> ( + match typ |> TypeUtils.extractObjectType ~env ~package with + | Some (env, tObj) -> + let rec getFields (texp : Types.type_expr) = + match texp.desc with + | Tfield (name, _, t1, t2) -> + let fields = t2 |> getFields in + (name, t1) :: fields + | Tlink te | Tsubst te | Tpoly (te, []) -> te |> getFields + | Tvar None -> [] + | _ -> [] + in + tObj |> getFields + |> Utils.filterMap (fun (field, typ) -> + if Utils.checkName field ~prefix:propertyName ~exact then + Some + (Completion.create field ~env ~kind:(Completion.ObjLabel typ)) + else None) + | None -> + if debugTypeLookups then + Printf.printf + "CObj: looked for an object but found something else: %s\n" + (Shared.typeToString typ); + []) + | None -> + if debugTypeLookups then + Printf.printf "CObj: could not look up type at ctx path\n"; + []) + | CPipe {functionCtxPath = cp; id = funNamePrefix; lhsLoc} -> ( + match + cp + |> getCompletionsForContextPath ~debug ~full ~opens ~rawOpens ~pos ~env + ~exact:true ~scope + |> completionsGetTypeEnv ~debug ~full ~opens ~rawOpens ~pos ~scope + with + | None -> + if debugTypeLookups then + Printf.printf "CPipe: could not look up type at pipe fn ctx path\n"; + [] + | Some (typ, envFromCompletionItem) -> ( + let env, typ = + typ + |> TypeUtils.resolveTypeForPipeCompletion ~env ~package ~full ~lhsLoc + in + if debug then + if env <> envFromCompletionItem then + Printf.printf "CPPipe env:%s envFromCompletionItem:%s\n" + (QueryEnv.toString env) + (QueryEnv.toString envFromCompletionItem) + else Printf.printf "CPPipe env:%s\n" (QueryEnv.toString env); + let completionPath = + match typ with + | Builtin (builtin, _) -> + let { + arrayModulePath; + optionModulePath; + stringModulePath; + intModulePath; + floatModulePath; + promiseModulePath; + listModulePath; + resultModulePath; + } = + package.builtInCompletionModules + in + Some + (match builtin with + | Array -> arrayModulePath + | Option -> optionModulePath + | String -> stringModulePath + | Int -> intModulePath + | Float -> floatModulePath + | Promise -> promiseModulePath + | List -> listModulePath + | Result -> resultModulePath + | Lazy -> ["Lazy"] + | Char -> ["Char"]) + | TypExpr t -> ( + match t.Types.desc with + | Tconstr (path, _typeArgs, _) + | Tlink {desc = Tconstr (path, _typeArgs, _)} + | Tsubst {desc = Tconstr (path, _typeArgs, _)} + | Tpoly ({desc = Tconstr (path, _typeArgs, _)}, []) -> ( + if debug then Printf.printf "CPPipe type path:%s\n" (Path.name path); + match Utils.expandPath path with + | _ :: pathRev -> + (* type path is relative to the completion environment + express it from the root of the file *) + let found, pathFromEnv = + QueryEnv.pathFromEnv envFromCompletionItem (List.rev pathRev) + in + if debug then + Printf.printf "CPPipe pathFromEnv:%s found:%b\n" + (pathFromEnv |> String.concat ".") + found; + if pathFromEnv = [] then None + else if + env.file.moduleName <> envFromCompletionItem.file.moduleName + && found + (* If the module names are different, then one needs to qualify the path. + But only if the path belongs to the env from completion *) + then Some (envFromCompletionItem.file.moduleName :: pathFromEnv) + else Some pathFromEnv + | _ -> None) + | _ -> None) + in + match completionPath with + | Some completionPath -> ( + let rec removeRawOpen rawOpen modulePath = + match (rawOpen, modulePath) with + | [_], _ -> Some modulePath + | s :: inner, first :: restPath when s = first -> + removeRawOpen inner restPath + | _ -> None + in + let rec removeRawOpens rawOpens modulePath = + match rawOpens with + | rawOpen :: restOpens -> ( + let newModulePath = removeRawOpens restOpens modulePath in + match removeRawOpen rawOpen newModulePath with + | None -> newModulePath + | Some mp -> mp) + | [] -> modulePath + in + let completionPathMinusOpens = + completionPath + |> removeRawOpens package.opens + |> removeRawOpens rawOpens |> String.concat "." + in + let completionName name = + if completionPathMinusOpens = "" then name + else completionPathMinusOpens ^ "." ^ name + in + let completions = + completionPath @ [funNamePrefix] + |> getCompletionsForPath ~debug ~completionContext:Value ~exact:false + ~package ~opens ~full ~pos ~env ~scope + in + let completions = + completions + |> List.map (fun (completion : Completion.t) -> + { + completion with + name = completionName completion.name; + env + (* Restore original env for the completion after x->foo()... *); + }) + in + (* We add React element functions to the completion if we're in a JSX context *) + let inJsx = false in + (* TODO(1) *) + let forJsxCompletion = + if inJsx then + match typ with + | Builtin (Int, t) -> Some ("int", t) + | Builtin (Float, t) -> Some ("float", t) + | Builtin (String, t) -> Some ("string", t) + | Builtin (Array, t) -> Some ("array", t) + | _ -> None + else None + in + match forJsxCompletion with + | Some (builtinNameToComplete, typ) + when Utils.checkName builtinNameToComplete ~prefix:funNamePrefix + ~exact:false -> + [ + Completion.createWithSnippet + ~name:("React." ^ builtinNameToComplete) + ~kind:(Value typ) ~env ~sortText:"A" + ~docstring: + [ + "Turns `" ^ builtinNameToComplete + ^ "` into `React.element` so it can be used inside of JSX."; + ] + (); + ] + @ completions + | _ -> completions) + | None -> [])) + | CTuple ctxPaths -> + (* Turn a list of context paths into a list of type expressions. *) + let typeExrps = + ctxPaths + |> List.map (fun contextPath -> + contextPath + |> getCompletionsForContextPath ~debug ~full ~opens ~rawOpens ~pos + ~env ~exact:true ~scope) + |> List.filter_map (fun completionItems -> + match completionItems with + | {Completion.kind = Value typ} :: _ -> Some typ + | _ -> None) + in + if List.length ctxPaths = List.length typeExrps then ( + if debugTypeLookups then + Printf.printf "CTuple: found tuple, returning it\n"; + [ + Completion.create "dummy" ~env + ~kind:(Completion.Value (Ctype.newty (Ttuple typeExrps))); + ]) + else ( + if debugTypeLookups then + Printf.printf + "CTuple: extracted tuple and target tuple length does not match\n"; + []) + | CJsxPropValue {pathToComponent; propName} -> ( + let findTypeOfValue path = + path + |> getCompletionsForPath ~debug ~completionContext:Value ~exact:true + ~package ~opens ~full ~pos ~env ~scope + |> completionsGetTypeEnv ~debug ~full ~opens ~rawOpens ~pos ~scope + in + let lowercaseComponent = + match pathToComponent with + | [elName] when Char.lowercase_ascii elName.[0] = elName.[0] -> true + | _ -> false + in + let targetLabel = + if lowercaseComponent then + let rec digToTypeForCompletion path = + match + path + |> getCompletionsForPath ~debug ~completionContext:Type ~exact:true + ~package ~opens ~full ~pos ~env ~scope + with + | {kind = Type {kind = Abstract (Some (p, _))}} :: _ -> + (* This case happens when what we're looking for is a type alias. + This is the case in newer rescript-react versions where + ReactDOM.domProps is an alias for JsxEvent.t. *) + let pathRev = p |> Utils.expandPath in + pathRev |> List.rev |> digToTypeForCompletion + | {kind = Type {kind = Record fields}} :: _ -> ( + match fields |> List.find_opt (fun f -> f.fname.txt = propName) with + | None -> None + | Some f -> Some (f.fname.txt, f.typ, env)) + | _ -> None + in + ["ReactDOM"; "domProps"] |> digToTypeForCompletion + else + CompletionJsx.getJsxLabels ~componentPath:pathToComponent + ~findTypeOfValue ~package + |> List.find_opt (fun (label, _, _) -> label = propName) + in + match targetLabel with + | None -> + if debugTypeLookups then + Printf.printf "CJsxPropValue: did not find target label\n"; + [] + | Some (_, typ, env) -> + if debugTypeLookups then + Printf.printf "CJsxPropValue: found type: %s\n" + (Shared.typeToString typ); + [ + Completion.create "dummy" ~env + ~kind:(Completion.Value (Utils.unwrapIfOption typ)); + ]) + | CFunctionArgument {functionContextPath; argumentLabel} -> ( + let labels, env = + match + functionContextPath + |> getCompletionsForContextPath ~debug ~full ~opens ~rawOpens ~pos ~env + ~exact:true ~scope + |> completionsGetCompletionType2 ~debug ~full ~opens ~rawOpens ~pos + ~scope + with + | Some ((TypeExpr typ | ExtractedType (Tfunction {typ})), env) -> + (typ |> TypeUtils.getArgs ~full ~env, env) + | _ -> + if debugTypeLookups then + Printf.printf + "CFunctionArgument: did not find fn type, or found type was \ + something other than a function\n"; + ([], env) + in + let targetLabel = + labels + |> List.find_opt (fun (label, _) -> + match (argumentLabel, label) with + | ( Unlabelled {argumentPosition = pos1}, + Completable.Unlabelled {argumentPosition = pos2} ) -> + pos1 = pos2 + | ( (Labelled name1 | Optional name1), + (Labelled name2 | Optional name2) ) -> + name1 = name2 + | _ -> false) + in + let expandOption = + match targetLabel with + | None | Some ((Unlabelled _ | Labelled _), _) -> false + | Some (Optional _, _) -> true + in + match targetLabel with + | None -> + if debugTypeLookups then + Printf.printf "CFunctionArgument: did not find target label\n"; + [] + | Some (_, typ) -> + if debugTypeLookups then + Printf.printf "CFunctionArgument: found type: %s\n" + (Shared.typeToString typ); + [ + Completion.create "dummy" ~env + ~kind: + (Completion.Value + (if expandOption then Utils.unwrapIfOption typ else typ)); + ]) + | CUnknown -> [] + | CVariantPayload {variantCtxPath; itemNum; constructorName} -> ( + match + variantCtxPath + |> getCompletionsForContextPath ~debug ~full ~opens ~rawOpens ~pos ~env + ~exact:true ~scope + |> completionsGetCompletionType2 ~debug ~full ~opens ~rawOpens ~pos ~scope + with + | Some (typ, env) -> ( + let typ = + match typ with + | ExtractedType inner -> Some inner + | TypeExpr t -> t |> TypeUtils.extractType ~env ~package:full.package + in + match typ with + | Some (Toption (env, innerType)) + when constructorName = "Some" && itemNum = 0 -> + (* Special handling for option which is represented as itself even though it's technically a variant. *) + [ + Completion.create "dummy" ~env + ~kind: + (match innerType with + | ExtractedType innerType -> ExtractedType (innerType, `Type) + | TypeExpr t -> Value t); + ] + | Some (Tvariant {constructors}) -> ( + let targetType = + constructors + |> Utils.findMap (fun (c : Constructor.t) -> + if c.cname.txt = constructorName then + match c.args with + | Args args -> ( + match List.nth_opt args itemNum with + | None -> None + | Some (typ, _) -> Some typ) + | _ -> None + else None) + in + match targetType with + | None -> + if debugTypeLookups then + Printf.printf + "CVariantPayload: could not find target payload type in variant \ + type\n"; + [] + | Some t -> + if debugTypeLookups then + Printf.printf "CVariantPayload: found payload type: %s\n" + (Shared.typeToString t); + [Completion.create "dummy" ~env ~kind:(Completion.Value t)]) + | Some t -> + if debugTypeLookups then + Printf.printf + "CVariantPayload(constructorName: %s, itemNum: %i): some other \ + type than a variant found at variant ctx path: %s\n" + constructorName itemNum + (TypeUtils.extractedTypeToString t); + [] + | None -> + if debugTypeLookups then + Printf.printf "CVariantPayload: no type found at variant ctx path\n"; + []) + | None -> + if debugTypeLookups then + Printf.printf "CVariantPayload: did not find type at variant ctx path\n"; + []) + | CTupleItem {tupleCtxPath; itemNum} -> ( + match + tupleCtxPath + |> getCompletionsForContextPath ~debug ~full ~opens ~rawOpens ~pos ~env + ~exact:true ~scope + |> completionsGetCompletionType2 ~debug ~full ~opens ~rawOpens ~pos ~scope + with + | Some (typ, env) -> ( + let typ = + match typ with + | ExtractedType t -> Some t + | TypeExpr t -> TypeUtils.extractType ~env ~package t + in + match typ with + | Some (Tuple (env, items, _)) -> ( + match List.nth_opt items itemNum with + | None -> + if debugTypeLookups then + Printf.printf + "CTupleItem: found tuple, but not the target item num\n"; + [] + | Some tupleItemType -> + if debugTypeLookups then + Printf.printf "CTupleItem: found tuple and item: %s\n" + (Shared.typeToString tupleItemType); + [Completion.create "dummy" ~env ~kind:(Value tupleItemType)]) + | Some t -> + if debugTypeLookups then + Printf.printf "CTupleItem: type, but it's not a tuple: %s\n" + (TypeUtils.extractedTypeToString t); + [] + | None -> + if debugTypeLookups then + Printf.printf "CTupleItem: no type extracted at tuple ctx path\n"; + []) + | None -> + if debugTypeLookups then + Printf.printf "CTupleItem: no type found at tuple ctx path\n"; + []) + | CRecordField {recordCtxPath} when true -> ( + let completionsForCtxPath = + recordCtxPath + |> getCompletionsForContextPath ~debug ~full ~opens ~rawOpens ~pos ~env + ~exact:true ~scope + in + let extracted = + match + completionsForCtxPath + |> completionsGetCompletionType2 ~debug ~full ~opens ~rawOpens ~pos + ~scope + with + | Some (TypeExpr typ, env) -> typ |> TypeUtils.extractType ~env ~package + | Some (ExtractedType typ, _env) -> Some typ + | None -> None + in + match extracted with + | Some (Trecord _ as typ) -> + if debugTypeLookups then + Printf.printf "CRecordField: found record: %s\n" + (TypeUtils.extractedTypeToString typ); + [Completion.create "dummy" ~env ~kind:(ExtractedType (typ, `Value))] + | Some t -> + if debugTypeLookups then + Printf.printf + "CRecordField: found something that's not a record at the record ctx \ + path: %s\n" + (TypeUtils.extractedTypeToString t); + [] + | None -> + if debugTypeLookups then + Printf.printf "CRecordField: found no type at record ctx path\n"; + []) + | CRecordField {recordCtxPath; prefix; seenFields} -> ( + let completionsForCtxPath = + recordCtxPath + |> getCompletionsForContextPath ~debug ~full ~opens ~rawOpens ~pos ~env + ~exact:true ~scope + in + let extracted = + match + completionsForCtxPath + |> completionsGetCompletionType2 ~debug ~full ~opens ~rawOpens ~pos + ~scope + with + | Some (TypeExpr typ, env) -> ( + match typ |> TypeUtils.extractRecordType ~env ~package with + | Some (env, fields, typDecl) -> + Some + ( env, + fields, + typDecl.item.decl |> Shared.declToString typDecl.name.txt ) + | None -> None) + | Some (ExtractedType typ, env) -> ( + match typ with + | Trecord {fields} -> + Printf.printf "fields: %s" + (fields |> List.map (fun (f : field) -> f.fname.txt) |> list); + Some (env, fields, typ |> TypeUtils.extractedTypeToString) + | _ -> None) + | None -> None + in + match extracted with + | None -> [] + | Some (env, fields, recordAsString) -> + let fields = + fields + |> Utils.filterMap (fun field -> + if + List.mem field.fname.txt seenFields = false + && Utils.checkName field.fname.txt ~prefix ~exact:false + then + Some + (Completion.create field.fname.txt ~env + ?deprecated:field.deprecated ~docstring:field.docstring + ~kind:(Completion.Field (field, recordAsString))) + else None) + in + Printf.printf "len: %i" (List.length fields); + fields) + | CRecordBody {recordCtxPath; seenFields} -> ( + let completionsForCtxPath = + recordCtxPath + |> getCompletionsForContextPath ~debug ~full ~opens ~rawOpens ~pos ~env + ~exact:true ~scope + in + let extracted = + match + completionsForCtxPath + |> completionsGetCompletionType2 ~debug ~full ~opens ~rawOpens ~pos + ~scope + with + | Some (TypeExpr typ, env) -> ( + match typ |> TypeUtils.extractRecordType ~env ~package with + | Some (env, fields, typDecl) -> + Some + ( env, + fields, + typDecl.item.decl |> Shared.declToString typDecl.name.txt ) + | None -> + if debugTypeLookups then + Printf.printf + "CRecordBody: found type at ctx path, but it's not a record: %s\n" + (Shared.typeToString typ); + None) + | Some (ExtractedType typ, env) -> ( + match typ with + | Trecord {fields} -> + Some (env, fields, typ |> TypeUtils.extractedTypeToString) + | t -> + if debugTypeLookups then + Printf.printf + "CRecordBody: found something that's not a record at ctx path: %s\n" + (TypeUtils.extractedTypeToString t); + None) + | None -> + if debugTypeLookups then + Printf.printf "CRecordBody: found no type at record ctx path\n"; + None + in + match extracted with + | None -> [] + | Some (env, fields, recordAsString) -> + if debugTypeLookups then + Printf.printf "CRecordBody: found record type, now returning fields\n"; + let fields = + fields + |> Utils.filterMap (fun field -> + if List.mem field.fname.txt seenFields = false then + Some + (Completion.create field.fname.txt ~env + ?deprecated:field.deprecated ~docstring:field.docstring + ~kind:(Completion.Field (field, recordAsString))) + else None) + in + fields) + | CFunction _ -> + (* TODO: Support more function stuff? Going from this to Tfunction *) [] + | CNone -> [] + | CRecordFieldFollow {recordCtxPath; fieldName} -> ( + match + recordCtxPath + |> getCompletionsForContextPath ~debug ~full ~opens ~rawOpens ~pos ~env + ~exact:true ~scope + |> completionsGetCompletionType2 ~debug ~full ~opens ~rawOpens ~pos ~scope + with + | Some (typ, env) -> ( + let typ = + match typ with + | ExtractedType t -> Some t + | TypeExpr t -> TypeUtils.extractType ~env ~package t + in + match typ with + | Some (Trecord {fields}) -> ( + match + fields + |> Utils.findMap (fun (field : field) -> + if field.fname.txt = fieldName then + Some + (if field.optional then Utils.unwrapIfOption field.typ + else field.typ) + else None) + with + | None -> + if debugTypeLookups then + Printf.printf + "CRecordFieldFollow: could not find the field to follow\n"; + [] + | Some fieldType -> + if debugTypeLookups then + Printf.printf + "CRecordFieldFollow: type of field (\"%s\") to follow: %s\n" + fieldName + (Shared.typeToString fieldType); + [Completion.create "dummy" ~env ~kind:(Value fieldType)]) + | Some t -> + if debugTypeLookups then + Printf.printf + "CRecordFieldFollow: found type at record ctx path, but it's not a \ + record: %s\n" + (TypeUtils.extractedTypeToString t); + [] + | None -> + if debugTypeLookups then + Printf.printf "CRecordFieldFollow: found no type at record ctx path\n"; + []) + | None -> + if debugTypeLookups then + Printf.printf "CRecordFieldFollow: found no type at record ctx path\n"; + []) + | CTypeAtLoc loc -> ( + match + References.getLocItem ~full ~pos:(Pos.ofLexing loc.loc_start) ~debug + with + | None -> + if debugTypeLookups then + Printf.printf "CTypeAtLoc: found no type at loc\n"; + [] + | Some {locType = Typed (_, typExpr, _)} -> + if debugTypeLookups then + Printf.printf "CTypeAtLoc: found type at loc: %s\n" + (Shared.typeToString typExpr); + [Completion.create "dummy" ~env ~kind:(Value typExpr)] + | Some _ -> + if debugTypeLookups then + Printf.printf + "CTypeAtLoc: found type at loc, but it's not something we can extract\n"; + []) + | CFunctionReturnType {functionCtxPath} -> ( + match functionCtxPath with + | CFunction {returnType} -> + returnType + |> getCompletionsForContextPath ~debug ~full ~opens ~rawOpens ~pos ~env + ~exact:true ~scope + | _ -> ( + match + functionCtxPath + |> getCompletionsForContextPath ~debug ~full ~opens ~rawOpens ~pos ~env + ~exact:true ~scope + |> completionsGetCompletionType2 ~debug ~full ~opens ~rawOpens ~pos + ~scope + with + | Some (ExtractedType (Tfunction {returnType}), env) -> + [Completion.create "dummy" ~env ~kind:(Completion.Value returnType)] + | Some (TypeExpr t, _) -> + if debugTypeLookups then + Printf.printf + "CFunctionReturnType: found type at fn ctx path, but it's not a \ + function: %s\n" + (Shared.typeToString t); + [] + | Some (ExtractedType t, _) -> + if debugTypeLookups then + Printf.printf + "CFunctionReturnType: found type at fn ctx path, but it's not a \ + function: %s\n" + (TypeUtils.extractedTypeToString t); + [] + | None -> + Printf.printf "CFunctionReturnType: found no type at fn ctx path\n"; + [])) + +type completionMode = Pattern of Completable.patternMode | Expression + +let rec completeTypedValue ~full ~prefix ~completionContext ~mode + (t : SharedTypes.completionType) = + match t with + | Tbool env -> + [ + Completion.create "true" ~kind:(Label "bool") ~env; + Completion.create "false" ~kind:(Label "bool") ~env; + ] + |> CompletionBackEnd.filterItems ~prefix + | Tvariant {env; constructors; variantDecl; variantName} -> + constructors + |> List.map (fun (constructor : Constructor.t) -> + let numArgs = + match constructor.args with + | InlineRecord _ -> 1 + | Args args -> List.length args + in + Completion.createWithSnippet ?deprecated:constructor.deprecated + ~name: + (constructor.cname.txt + ^ CompletionBackEnd.printConstructorArgs numArgs ~asSnippet:false + ) + ~insertText: + (constructor.cname.txt + ^ CompletionBackEnd.printConstructorArgs numArgs ~asSnippet:true + ) + ~kind: + (Constructor + (constructor, variantDecl |> Shared.declToString variantName)) + ~env ()) + |> CompletionBackEnd.filterItems ~prefix + | Tpolyvariant {env; constructors; typeExpr} -> + constructors + |> List.map (fun (constructor : polyVariantConstructor) -> + Completion.createWithSnippet + ~name: + ("#" ^ constructor.name + ^ CompletionBackEnd.printConstructorArgs + (List.length constructor.args) + ~asSnippet:false) + ~insertText: + ((if Utils.startsWith prefix "#" then "" else "#") + ^ constructor.name + ^ CompletionBackEnd.printConstructorArgs + (List.length constructor.args) + ~asSnippet:true) + ~kind: + (PolyvariantConstructor + (constructor, typeExpr |> Shared.typeToString)) + ~env ()) + |> CompletionBackEnd.filterItems ~prefix + | Toption (env, t) -> + let innerType = + match t with + | ExtractedType t -> Some t + | TypeExpr t -> t |> TypeUtils.extractType ~env ~package:full.package + in + let expandedCompletions = + match innerType with + | None -> [] + | Some innerType -> + innerType + |> completeTypedValue ~full ~prefix ~completionContext ~mode + |> List.map (fun (c : Completion.t) -> + { + c with + name = "Some(" ^ c.name ^ ")"; + sortText = None; + insertText = + (match c.insertText with + | None -> None + | Some insertText -> Some ("Some(" ^ insertText ^ ")")); + }) + in + let noneCase = Completion.create "None" ~kind:(kindFromInnerType t) ~env in + let someAnyCase = + Completion.createWithSnippet ~name:"Some(_)" ~kind:(kindFromInnerType t) + ~env ~insertText:"Some(${1:_})" () + in + let completions = + match completionContext with + | Some (Completable.CameFromRecordField fieldName) -> + [ + Completion.createWithSnippet + ~name:("Some(" ^ fieldName ^ ")") + ~kind:(kindFromInnerType t) ~env + ~insertText:("Some(${1:" ^ fieldName ^ "})") + (); + someAnyCase; + noneCase; + ] + | _ -> [noneCase; someAnyCase] + in + completions @ expandedCompletions |> CompletionBackEnd.filterItems ~prefix + | Tuple (env, exprs, typ) -> + let numExprs = List.length exprs in + [ + Completion.createWithSnippet + ~name:(CompletionBackEnd.printConstructorArgs numExprs ~asSnippet:false) + ~insertText: + (CompletionBackEnd.printConstructorArgs numExprs ~asSnippet:true) + ~kind:(Value typ) ~env (); + ] + | Trecord {env; fields} as extractedType -> ( + (* As we're completing for a record, we'll need a hint (completionContext) + here to figure out whether we should complete for a record field, or + the record body itself. *) + match completionContext with + | Some (Completable.RecordField {seenFields}) -> + fields + |> List.filter (fun (field : field) -> + List.mem field.fname.txt seenFields = false) + |> List.map (fun (field : field) -> + match (field.optional, mode) with + | true, Pattern Destructuring -> + Completion.create ("?" ^ field.fname.txt) + ?deprecated:field.deprecated + ~docstring: + [ + field.fname.txt + ^ " is an optional field, and needs to be destructured \ + using '?'."; + ] + ~kind: + (Field (field, TypeUtils.extractedTypeToString extractedType)) + ~env + | _ -> + Completion.create field.fname.txt ?deprecated:field.deprecated + ~kind: + (Field (field, TypeUtils.extractedTypeToString extractedType)) + ~env) + |> CompletionBackEnd.filterItems ~prefix + | _ -> + if prefix = "" then + [ + Completion.createWithSnippet ~name:"{}" + ~insertText:(if !Cfg.supportsSnippets then "{$0}" else "{}") + ~sortText:"A" + ~kind: + (ExtractedType + ( extractedType, + match mode with + | Pattern _ -> `Type + | Expression -> `Value )) + ~env (); + ] + else []) + | TinlineRecord {env; fields} -> ( + match completionContext with + | Some (Completable.RecordField {seenFields}) -> + fields + |> List.filter (fun (field : field) -> + List.mem field.fname.txt seenFields = false) + |> List.map (fun (field : field) -> + Completion.create field.fname.txt ~kind:(Label "Inline record") + ?deprecated:field.deprecated ~env) + |> CompletionBackEnd.filterItems ~prefix + | _ -> + if prefix = "" then + [ + Completion.createWithSnippet ~name:"{}" + ~insertText:(if !Cfg.supportsSnippets then "{$0}" else "{}") + ~sortText:"A" ~kind:(Label "Inline record") ~env (); + ] + else []) + | Tarray (env, typ) -> + if prefix = "" then + [ + Completion.createWithSnippet ~name:"[]" + ~insertText:(if !Cfg.supportsSnippets then "[$0]" else "[]") + ~sortText:"A" + ~kind: + (match typ with + | ExtractedType typ -> + ExtractedType + ( typ, + match mode with + | Pattern _ -> `Type + | Expression -> `Value ) + | TypeExpr typ -> Value typ) + ~env (); + ] + else [] + | Tstring env -> + if prefix = "" then + [ + Completion.createWithSnippet ~name:"\"\"" + ~insertText:(if !Cfg.supportsSnippets then "\"$0\"" else "\"\"") + ~sortText:"A" + ~kind: + (Value (Ctype.newconstr (Path.Pident (Ident.create "string")) [])) + ~env (); + ] + else [] + | Tfunction {env; typ; args; uncurried} when prefix = "" && mode = Expression + -> + let shouldPrintAsUncurried = uncurried && !Config.uncurried <> Uncurried in + let mkFnArgs ~asSnippet = + match args with + | [(Nolabel, argTyp)] when TypeUtils.typeIsUnit argTyp -> + if shouldPrintAsUncurried then "(. )" else "()" + | [(Nolabel, argTyp)] -> + let varName = + CompletionExpressions.prettyPrintFnTemplateArgName ~env ~full argTyp + in + let argsText = if asSnippet then "${1:" ^ varName ^ "}" else varName in + if shouldPrintAsUncurried then "(. " ^ argsText ^ ")" else argsText + | _ -> + let currentUnlabelledIndex = ref 0 in + let argsText = + args + |> List.map (fun ((label, typ) : typedFnArg) -> + match label with + | Optional name -> "~" ^ name ^ "=?" + | Labelled name -> "~" ^ name + | Nolabel -> + if TypeUtils.typeIsUnit typ then "()" + else ( + currentUnlabelledIndex := !currentUnlabelledIndex + 1; + let num = !currentUnlabelledIndex in + let varName = + CompletionExpressions.prettyPrintFnTemplateArgName + ~currentIndex:num ~env ~full typ + in + if asSnippet then + "${" ^ string_of_int num ^ ":" ^ varName ^ "}" + else varName)) + |> String.concat ", " + in + "(" ^ if shouldPrintAsUncurried then ". " else "" ^ argsText ^ ")" + in + [ + Completion.createWithSnippet + ~name:(mkFnArgs ~asSnippet:false ^ " => {}") + ~insertText: + (mkFnArgs ~asSnippet:!Cfg.supportsSnippets + ^ " => " + ^ if !Cfg.supportsSnippets then "{$0}" else "{}") + ~sortText:"A" ~kind:(Value typ) ~env (); + ] + | Tfunction _ -> [] + | Texn env -> + [ + Completion.create + (full.package.builtInCompletionModules.exnModulePath @ ["Error(error)"] + |> ident) + ~kind:(Label "Catches errors from JavaScript errors.") + ~docstring: + [ + "Matches on a JavaScript error. Read more in the [documentation on \ + catching JS \ + exceptions](https://rescript-lang.org/docs/manual/latest/exception#catching-js-exceptions)."; + ] + ~env; + ] + | Tpromise _ -> [] + +let rec processCompletable ~debug ~full ~scope ~env ~pos ~forHover completable = + if debug then + Printf.printf "Completable: %s\n" + (CompletionInstruction.toString completable); + let package = full.package in + let rawOpens = Scope.getRawOpens scope in + let opens = getOpens ~debug ~rawOpens ~package ~env in + let allFiles = allFilesInPackage package in + let findTypeOfValue path = + path + |> getCompletionsForPath ~debug ~completionContext:Value ~exact:true + ~package ~opens ~full ~pos ~env ~scope + |> completionsGetTypeEnv ~debug ~full ~opens ~rawOpens ~pos ~scope + in + match completable with + | Cnone -> [] + | CtxPath contextPath -> + contextPath + |> getCompletionsForContextPath ~debug ~full ~opens ~rawOpens ~pos ~env + ~exact:forHover ~scope + | Cjsx {pathToComponent = [id]; prefix; seenProps = identsSeen} + when String.uncapitalize_ascii id = id -> + (* Lowercase JSX tag means builtin *) + let mkLabel (name, typString) = + Completion.create name ~kind:(Label typString) ~env + in + let keyLabels = + if Utils.startsWith "key" prefix then [mkLabel ("key", "string")] else [] + in + (CompletionJsx.domLabels + |> List.filter (fun (name, _t) -> + Utils.startsWith name prefix + && (forHover || not (List.mem name identsSeen))) + |> List.map mkLabel) + @ keyLabels + | Cjsx {pathToComponent = componentPath; prefix; seenProps = identsSeen} -> + let labels = + CompletionJsx.getJsxLabels ~componentPath ~findTypeOfValue ~package + in + let mkLabel_ name typString = + Completion.create name ~kind:(Label typString) ~env + in + let mkLabel (name, typ, _env) = + mkLabel_ name (typ |> Shared.typeToString) + in + let keyLabels = + if Utils.startsWith "key" prefix then [mkLabel_ "key" "string"] else [] + in + if labels = [] then [] + else + (labels + |> List.filter (fun (name, _t, _env) -> + Utils.startsWith name prefix + && name <> "key" + && (forHover || not (List.mem name identsSeen))) + |> List.map mkLabel) + @ keyLabels + (* | Cdecorator prefix -> + let mkDecorator (name, docstring) = + {(Completion.create name ~kind:(Label "") ~env) with docstring} + in + let isTopLevel = String.starts_with ~prefix:"@" prefix in + let prefix = + if isTopLevel then String.sub prefix 1 (String.length prefix - 1) + else prefix + in + let decorators = + if isTopLevel then CompletionDecorators.toplevel + else CompletionDecorators.local + in + decorators + |> List.filter (fun (decorator, _) -> Utils.startsWith decorator prefix) + |> List.map (fun (decorator, doc) -> + let parts = String.split_on_char '.' prefix in + let len = String.length prefix in + let dec2 = + if List.length parts > 1 then + String.sub decorator len (String.length decorator - len) + else decorator + in + (dec2, doc)) + |> List.map mkDecorator*) + | CnamedArg {ctxPath; prefix; seenLabels} -> + let labels = + match + ctxPath + |> getCompletionsForContextPath ~debug ~full ~opens ~rawOpens ~pos ~env + ~exact:true ~scope + |> completionsGetTypeEnv ~debug ~full ~opens ~rawOpens ~pos ~scope + with + | Some (typ, _env) -> + if debug then + Printf.printf "Found type for function %s\n" + (typ |> Shared.typeToString); + + typ + |> TypeUtils.getArgs ~full ~env + |> List.filter_map (fun arg -> + match arg with + | SharedTypes.Completable.Labelled name, a -> Some (name, a) + | Optional name, a -> Some (name, a) + | _ -> None) + | None -> [] + in + let mkLabel (name, typ) = + Completion.create name ~kind:(Label (typ |> Shared.typeToString)) ~env + in + labels + |> List.filter (fun (name, _t) -> + Utils.startsWith name prefix + && (forHover || not (List.mem name seenLabels))) + |> List.map mkLabel + | Cpattern {ctxPath; prefix} -> ( + match + ctxPath + |> getCompletionsForContextPath ~debug ~full ~opens ~rawOpens ~pos ~env + ~exact:true ~scope + |> completionsGetCompletionType2 ~debug ~full ~opens ~rawOpens ~pos ~scope + with + | Some (typ, env) -> ( + let typ = + match typ with + | ExtractedType inner -> Some inner + | TypeExpr t -> t |> TypeUtils.extractType ~env ~package:full.package + in + match typ with + | None -> [] + | Some typ -> + let items = + typ + |> completeTypedValue ~mode:(Pattern Default) ~full ~prefix + ~completionContext:None + in + items) + | None -> []) + | Cexpression {ctxPath; prefix} -> ( + (* 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 + ctxPath + |> getCompletionsForContextPath ~debug ~full ~opens ~rawOpens ~pos ~env + ~exact:true ~scope + |> completionsGetCompletionType2 ~full ~debug ~opens ~rawOpens ~pos ~scope + with + | None -> regularCompletions + | Some (typ, _env) -> ( + let extractedType = + match typ with + | ExtractedType t -> Some t + | TypeExpr t -> TypeUtils.extractType t ~env ~package:full.package + in + match extractedType with + | None -> regularCompletions + | Some typ -> ( + (* TODO: We can get rid of the completion context and only use the ctx path *) + let completionContext = + match ctxPath with + | CRecordBody {seenFields} | CRecordField {seenFields} -> + Some (Completable.RecordField {seenFields}) + | CRecordFieldFollow {fieldName} -> + Some (CameFromRecordField fieldName) + | _ -> None + in + let wrapInsertTextInBraces = + if List.length [] > 0 then false + else + match ctxPath with + | CJsxPropValue _ -> true + | _ -> false + in + let items = + typ + |> completeTypedValue ~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))) + (*| CexhaustiveSwitch {contextPath; exprLoc} -> + let range = Utils.rangeOfLoc exprLoc in + let printFailwithStr num = + "${" ^ string_of_int num ^ ":failwith(\"todo\")}" + in + let withExhaustiveItem ~cases ?(startIndex = 0) (c : Completion.t) = + (* We don't need to write out `switch` here since we know that's what the + user has already written. Just complete for the rest. *) + let newText = + c.name ^ " {\n" + ^ (cases + |> List.mapi (fun index caseText -> + "| " ^ caseText ^ " => " + ^ printFailwithStr (startIndex + index + 1)) + |> String.concat "\n") + ^ "\n}" + |> Utils.indent range.start.character + in + [ + c; + { + c with + name = c.name ^ " (exhaustive switch)"; + filterText = Some c.name; + insertTextFormat = Some Snippet; + insertText = Some newText; + kind = Snippet "insert exhaustive switch for value"; + }; + ] + in + let completionsForContextPath = + contextPath + |> getCompletionsForContextPath ~debug ~full ~opens ~rawOpens ~pos ~env + ~exact:forHover ~scope + in + completionsForContextPath + |> List.map (fun (c : Completion.t) -> + match c.kind with + | Value typExpr -> ( + match typExpr |> TypeUtils.extractType ~env:c.env ~package with + | Some (Tvariant v) -> + withExhaustiveItem c + ~cases: + (v.constructors + |> List.map (fun (constructor : Constructor.t) -> + constructor.cname.txt + ^ + match constructor.args with + | Args [] -> "" + | _ -> "(_)")) + | Some (Tpolyvariant v) -> + withExhaustiveItem c + ~cases: + (v.constructors + |> List.map (fun (constructor : polyVariantConstructor) -> + "#" ^ constructor.name + ^ + match constructor.args with + | [] -> "" + | _ -> "(_)")) + | Some (Toption (_env, _typ)) -> + withExhaustiveItem c ~cases:["Some($1)"; "None"] ~startIndex:1 + | Some (Tbool _) -> withExhaustiveItem c ~cases:["true"; "false"] + | _ -> [c]) + | _ -> [c]) + |> List.flatten*) + | ChtmlElement {prefix} -> + CompletionJsx.htmlElements + |> List.filter_map (fun (elementName, description, deprecated) -> + if Utils.startsWith elementName prefix then + let name = "<" ^ elementName ^ ">" in + Some + (Completion.create name ~kind:(Label name) ~detail:description + ~env ~docstring:[description] ~insertText:elementName + ?deprecated: + (match deprecated with + | true -> Some "true" + | false -> None)) + else None) diff --git a/analysis/src/CompletionFrontEnd.ml b/analysis/src/CompletionFrontEnd.ml index 70ce8eda5..d05991cb6 100644 --- a/analysis/src/CompletionFrontEnd.ml +++ b/analysis/src/CompletionFrontEnd.ml @@ -318,11 +318,13 @@ let completionWithParser1 ~currentFile ~debug ~offset ~path ~posCursor (pat : Parsetree.pattern) = let contextPathToSave = match (contextPath, patternPath) with - | maybeContextPath, [] -> maybeContextPath + | Some ctxPath, [] -> Some (Scope.Completable ctxPath) + | None, [] -> None | Some contextPath, patternPath -> Some - (Completable.CPatternPath - {rootCtxPath = contextPath; nested = List.rev patternPath}) + (Completable + (Completable.CPatternPath + {rootCtxPath = contextPath; nested = List.rev patternPath})) | _ -> None in match pat.ppat_desc with @@ -336,7 +338,7 @@ let completionWithParser1 ~currentFile ~debug ~offset ~path ~posCursor if contextPathToSave = None then match p with | {ppat_desc = Ppat_var {txt}} -> - Some (Completable.CPId ([txt], Value)) + Some (Scope.Completable (Completable.CPId ([txt], Value))) | _ -> None else None in diff --git a/analysis/src/CompletionFrontEndNew.ml b/analysis/src/CompletionFrontEndNew.ml new file mode 100644 index 000000000..6114316b8 --- /dev/null +++ b/analysis/src/CompletionFrontEndNew.ml @@ -0,0 +1,1179 @@ +open SharedTypes +open CompletionNewTypes +open CompletionsNewTypesCtxPath + +let flattenLidCheckDot ?(jsx = true) ~(completionContext : CompletionContext.t) + (lid : Longident.t Location.loc) = + (* Flatten an identifier keeping track of whether the current cursor + is after a "." in the id followed by a blank character. + In that case, cut the path after ".". *) + let cutAtOffset = + let idStart = Loc.start lid.loc in + match completionContext.positionContext.whitespaceAfterCursor with + | Some '.' -> + if fst completionContext.positionContext.beforeCursor = fst idStart then + Some (snd completionContext.positionContext.beforeCursor - snd idStart) + else None + | _ -> None + in + Utils.flattenLongIdent ~cutAtOffset ~jsx lid.txt + +let ctxPathFromCompletionContext (completionContext : CompletionContext.t) = + completionContext.ctxPath + +(** This is for when you want a context path for an expression, without necessarily wanting + to do completion in that expression. For instance when completing patterns + `let {} = someRecordVariable`, we want the context path to `someRecordVariable` to + be able to figure out the type we're completing in the pattern. *) +let rec exprToContextPathInner (e : Parsetree.expression) = + match e.pexp_desc with + | Pexp_constant (Pconst_string _) -> Some CString + | Pexp_constant (Pconst_integer _) -> Some CInt + | Pexp_constant (Pconst_float _) -> Some CFloat + | Pexp_construct ({txt = Lident ("true" | "false")}, None) -> Some CBool + | Pexp_array exprs -> + Some + (CArray + (match exprs with + | [] -> None + | exp :: _ -> exprToContextPath exp)) + | Pexp_ident {txt = Lident ("|." | "|.u")} -> None + | Pexp_ident {txt} -> Some (CId (Utils.flattenLongIdent txt, Value)) + | Pexp_field (e1, {txt = Lident name}) -> ( + match exprToContextPath e1 with + | Some contextPath -> + Some (CRecordFieldAccess {recordCtxPath = contextPath; fieldName = name}) + | _ -> None) + | Pexp_field (_, {txt = Ldot (lid, name)}) -> + (* Case x.M.field ignore the x part *) + Some + (CRecordFieldAccess + { + recordCtxPath = CId (Utils.flattenLongIdent lid, Module); + fieldName = name; + }) + | Pexp_send (e1, {txt}) -> ( + match exprToContextPath e1 with + | None -> None + | Some contexPath -> + Some (CObj {objectCtxPath = contexPath; propertyName = txt})) + | Pexp_apply + ( {pexp_desc = Pexp_ident {txt = Lident ("|." | "|.u")}}, + [ + (_, lhs); + (_, {pexp_desc = Pexp_apply (d, args); pexp_loc; pexp_attributes}); + ] ) -> + (* Transform away pipe with apply call *) + exprToContextPath + { + pexp_desc = Pexp_apply (d, (Nolabel, lhs) :: args); + pexp_loc; + pexp_attributes; + } + | Pexp_apply + ( {pexp_desc = Pexp_ident {txt = Lident ("|." | "|.u")}}, + [(_, lhs); (_, {pexp_desc = Pexp_ident id; pexp_loc; pexp_attributes})] + ) -> + (* Transform away pipe with identifier *) + exprToContextPath + { + pexp_desc = + Pexp_apply + ( {pexp_desc = Pexp_ident id; pexp_loc; pexp_attributes}, + [(Nolabel, lhs)] ); + pexp_loc; + pexp_attributes; + } + | Pexp_apply (e1, args) -> ( + match exprToContextPath e1 with + | None -> None + | Some contexPath -> + Some (CApply {functionCtxPath = contexPath; args = args |> List.map fst})) + | Pexp_tuple exprs -> + let exprsAsContextPaths = exprs |> List.filter_map exprToContextPath in + if List.length exprs = List.length exprsAsContextPaths then + Some (CTuple exprsAsContextPaths) + else None + | _ -> None + +and exprToContextPath (e : Parsetree.expression) = + match + ( Res_parsetree_viewer.hasAwaitAttribute e.pexp_attributes, + exprToContextPathInner e ) + with + | true, Some ctxPath -> Some (CAwait ctxPath) + | false, Some ctxPath -> Some ctxPath + | _, None -> None + +let rec ctxPathFromCoreType ~completionContext (coreType : Parsetree.core_type) + = + match coreType.ptyp_desc with + | Ptyp_constr ({txt = Lident "option"}, [innerTyp]) -> + innerTyp + |> ctxPathFromCoreType ~completionContext + |> Option.map (fun innerTyp -> COption innerTyp) + | Ptyp_constr ({txt = Lident "array"}, [innerTyp]) -> + Some (CArray (innerTyp |> ctxPathFromCoreType ~completionContext)) + | Ptyp_constr ({txt = Lident "bool"}, []) -> Some CBool + | Ptyp_constr ({txt = Lident "int"}, []) -> Some CInt + | Ptyp_constr ({txt = Lident "float"}, []) -> Some CFloat + | Ptyp_constr ({txt = Lident "string"}, []) -> Some CString + | Ptyp_constr (lid, []) -> + Some (CId (lid |> flattenLidCheckDot ~completionContext, Type)) + | Ptyp_tuple types -> + let types = + types + |> List.map (fun (t : Parsetree.core_type) -> + match t |> ctxPathFromCoreType ~completionContext with + | None -> CUnknown + | Some ctxPath -> ctxPath) + in + Some (CTuple types) + | Ptyp_arrow _ -> ( + let rec loopFnTyp (ct : Parsetree.core_type) = + match ct.ptyp_desc with + | Ptyp_arrow (_arg, _argTyp, nextTyp) -> loopFnTyp nextTyp + | _ -> ct + in + let returnType = loopFnTyp coreType in + match ctxPathFromCoreType ~completionContext returnType with + | None -> None + | Some returnType -> Some (CFunction {returnType})) + | _ -> None + +let findCurrentlyLookingForInPattern ~completionContext + (pat : Parsetree.pattern) = + match pat.ppat_desc with + | Ppat_constraint (_pat, typ) -> ctxPathFromCoreType ~completionContext typ + | _ -> None + +(* An expression with that's an expr hole and that has an empty cursor. TODO Explain *) +let checkIfExprHoleEmptyCursor ~(completionContext : CompletionContext.t) + (exp : Parsetree.expression) = + CompletionExpressions.isExprHole exp + && CursorPosition.classifyLoc exp.pexp_loc + ~pos:completionContext.positionContext.beforeCursor + = EmptyLoc + +let checkIfPatternHoleEmptyCursor ~(completionContext : CompletionContext.t) + (pat : Parsetree.pattern) = + CompletionPatterns.isPatternHole pat + && CursorPosition.classifyLoc pat.ppat_loc + ~pos:completionContext.positionContext.beforeCursor + = EmptyLoc + +let completePipeChain (exp : Parsetree.expression) = + (* Complete the end of pipe chains by reconstructing the pipe chain as a single pipe, + so it can be completed. + Example: + someArray->Js.Array2.filter(v => v > 10)->Js.Array2.map(v => v + 2)-> + will complete as: + Js.Array2.map(someArray->Js.Array2.filter(v => v > 10), v => v + 2)-> + *) + match exp.pexp_desc with + (* When the left side of the pipe we're completing is a function application. + Example: someArray->Js.Array2.map(v => v + 2)-> *) + | Pexp_apply + ( {pexp_desc = Pexp_ident {txt = Lident ("|." | "|.u")}}, + [_; (_, {pexp_desc = Pexp_apply (d, _)})] ) -> + exprToContextPath exp |> Option.map (fun ctxPath -> (ctxPath, d.pexp_loc)) + (* When the left side of the pipe we're completing is an identifier application. + Example: someArray->filterAllTheGoodStuff-> *) + | Pexp_apply + ( {pexp_desc = Pexp_ident {txt = Lident ("|." | "|.u")}}, + [_; (_, {pexp_desc = Pexp_ident _; pexp_loc})] ) -> + exprToContextPath exp |> Option.map (fun ctxPath -> (ctxPath, pexp_loc)) + | _ -> None + +let completePipe ~id (lhs : Parsetree.expression) = + match completePipeChain lhs with + | Some (pipe, lhsLoc) -> Some (CPipe {functionCtxPath = pipe; id; lhsLoc}) + | None -> ( + match exprToContextPath lhs with + | Some pipe -> + Some (CPipe {functionCtxPath = pipe; id; lhsLoc = lhs.pexp_loc}) + | None -> None) + +(** Scopes *) +let rec scopePattern ~scope (pat : Parsetree.pattern) = + match pat.ppat_desc with + | Ppat_any -> scope + | Ppat_var {txt; loc} -> scope |> Scope.addValue ~name:txt ~loc + | Ppat_alias (p, asA) -> + let scope = scopePattern p ~scope in + scope |> Scope.addValue ~name:asA.txt ~loc:asA.loc + | Ppat_constant _ | Ppat_interval _ -> scope + | Ppat_tuple pl -> + pl |> List.map (fun p -> scopePattern p ~scope) |> List.concat + | Ppat_construct (_, None) -> scope + | Ppat_construct (_, Some {ppat_desc = Ppat_tuple pl}) -> + pl |> List.map (fun p -> scopePattern p ~scope) |> List.concat + | Ppat_construct (_, Some p) -> scopePattern ~scope p + | Ppat_variant (_, None) -> scope + | Ppat_variant (_, Some {ppat_desc = Ppat_tuple pl}) -> + pl |> List.map (fun p -> scopePattern p ~scope) |> List.concat + | Ppat_variant (_, Some p) -> scopePattern ~scope p + | Ppat_record (fields, _) -> + fields + |> List.map (fun (fname, p) -> + match fname with + | {Location.txt = Longident.Lident _fname} -> scopePattern ~scope p + | _ -> []) + |> List.concat + | Ppat_array pl -> + pl + |> List.map (fun (p : Parsetree.pattern) -> scopePattern ~scope p) + |> List.concat + | Ppat_or (p1, _) -> scopePattern ~scope p1 + | Ppat_constraint (p, _coreType) -> scopePattern ~scope p + | Ppat_type _ -> scope + | Ppat_lazy p -> scopePattern ~scope p + | Ppat_unpack {txt; loc} -> scope |> Scope.addValue ~name:txt ~loc + | Ppat_exception p -> scopePattern ~scope p + | Ppat_extension _ -> scope + | Ppat_open (_, p) -> scopePattern ~scope p + +let scopeValueBinding ~scope (vb : Parsetree.value_binding) = + scopePattern ~scope vb.pvb_pat + +let scopeValueBindings ~scope (valueBindings : Parsetree.value_binding list) = + let newScope = ref scope in + valueBindings + |> List.iter (fun (vb : Parsetree.value_binding) -> + newScope := scopeValueBinding vb ~scope:!newScope); + !newScope + +let scopeTypeKind ~scope (tk : Parsetree.type_kind) = + match tk with + | Ptype_variant constrDecls -> + constrDecls + |> List.map (fun (cd : Parsetree.constructor_declaration) -> + scope |> Scope.addConstructor ~name:cd.pcd_name.txt ~loc:cd.pcd_loc) + |> List.concat + | Ptype_record labelDecls -> + labelDecls + |> List.map (fun (ld : Parsetree.label_declaration) -> + scope |> Scope.addField ~name:ld.pld_name.txt ~loc:ld.pld_loc) + |> List.concat + | _ -> scope + +let scopeTypeDeclaration ~scope (td : Parsetree.type_declaration) = + let scope = + scope |> Scope.addType ~name:td.ptype_name.txt ~loc:td.ptype_name.loc + in + scopeTypeKind ~scope td.ptype_kind + +let scopeTypeDeclarations ~scope + (typeDeclarations : Parsetree.type_declaration list) = + let newScope = ref scope in + typeDeclarations + |> List.iter (fun (td : Parsetree.type_declaration) -> + newScope := scopeTypeDeclaration td ~scope:!newScope); + !newScope + +let scopeModuleBinding ~scope (mb : Parsetree.module_binding) = + scope |> Scope.addModule ~name:mb.pmb_name.txt ~loc:mb.pmb_name.loc + +let scopeModuleDeclaration ~scope (md : Parsetree.module_declaration) = + scope |> Scope.addModule ~name:md.pmd_name.txt ~loc:md.pmd_name.loc + +let scopeValueDescription ~scope (vd : Parsetree.value_description) = + scope |> Scope.addValue ~name:vd.pval_name.txt ~loc:vd.pval_name.loc + +let scopeStructureItem ~scope (item : Parsetree.structure_item) = + match item.pstr_desc with + | Pstr_value (_, valueBindings) -> scopeValueBindings ~scope valueBindings + | Pstr_type (_, typeDeclarations) -> + scopeTypeDeclarations ~scope typeDeclarations + | Pstr_open {popen_lid} -> scope |> Scope.addOpen ~lid:popen_lid.txt + | Pstr_primitive vd -> scopeValueDescription ~scope vd + | _ -> scope + +let rec completeFromStructure ~(completionContext : CompletionContext.t) + (structure : Parsetree.structure) : CompletionResult.t = + let scope = ref completionContext.scope in + structure + |> Utils.findMap (fun (item : Parsetree.structure_item) -> + let res = + completeStructureItem + ~completionContext: + (CompletionContext.withScope !scope completionContext) + item + in + scope := scopeStructureItem ~scope:!scope item; + res) + +and completeStructureItem ~(completionContext : CompletionContext.t) + (item : Parsetree.structure_item) : CompletionResult.t = + let locHasPos = completionContext.positionContext.locHasPos in + match item.pstr_desc with + | Pstr_value (recFlag, valueBindings) -> + if locHasPos item.pstr_loc then + completeValueBindings ~completionContext ~recFlag valueBindings + else None + | Pstr_eval _ | Pstr_primitive _ | Pstr_type _ | Pstr_typext _ + | Pstr_exception _ | Pstr_module _ | Pstr_recmodule _ | Pstr_modtype _ + | Pstr_open _ | Pstr_include _ | Pstr_attribute _ | Pstr_extension _ -> + None + | Pstr_class _ | Pstr_class_type _ -> + (* These aren't relevant for ReScript *) None + +and completeValueBinding ~(completionContext : CompletionContext.t) + (vb : Parsetree.value_binding) : CompletionResult.t = + let locHasPos = completionContext.positionContext.locHasPos in + let bindingConstraint = + findCurrentlyLookingForInPattern ~completionContext vb.pvb_pat + in + (* Always reset the context when completing value bindings, + since they create their own context. *) + let completionContext = CompletionContext.resetCtx completionContext in + if locHasPos vb.pvb_pat.ppat_loc then + (* Completing the pattern of the binding. `let {} = someRecordVariable`. + Ensure the context carries the root type of `someRecordVariable`. *) + let completionContext = + CompletionContext.currentlyExpectingOrTypeAtLoc ~loc:vb.pvb_expr.pexp_loc + (exprToContextPath vb.pvb_expr) + completionContext + in + completePattern ~completionContext vb.pvb_pat + else if locHasPos vb.pvb_loc then + (* First try completing the expression. *) + (* A let binding expression either has the constraint of the binding, + or an inferred constraint (if it has been compiled), or no constraint. *) + let completionContextForExprCompletion = + completionContext + |> CompletionContext.currentlyExpectingOrTypeAtLoc2 + ~loc:vb.pvb_pat.ppat_loc bindingConstraint + in + let completedExpression = + completeExpr ~completionContext:completionContextForExprCompletion + vb.pvb_expr + in + match completedExpression with + | Some res -> Some res + | None -> + (* In the binding but not in the pattern or expression means parser error recovery. + We can still complete the pattern or expression if we have enough information. *) + let exprHole = + checkIfExprHoleEmptyCursor + ~completionContext:completionContextForExprCompletion vb.pvb_expr + in + let patHole = + checkIfPatternHoleEmptyCursor + ~completionContext:completionContextForExprCompletion vb.pvb_pat + in + let exprCtxPath = exprToContextPath vb.pvb_expr in + (* Try the expression. Example: `let someVar: someType = *) + if exprHole then + let completionContext = + completionContextForExprCompletion + |> CompletionContext.currentlyExpectingOrTypeAtLoc + ~loc:vb.pvb_pat.ppat_loc bindingConstraint + in + CompletionResult.ctxPath (CId ([], Value)) completionContext + else if patHole then + let completionContext = + CompletionContext.currentlyExpectingOrTypeAtLoc + ~loc:vb.pvb_expr.pexp_loc exprCtxPath + completionContextForExprCompletion + in + CompletionResult.pattern ~prefix:"" ~completionContext + else None + else None + +and completeValueBindings ~(completionContext : CompletionContext.t) + ~(recFlag : Asttypes.rec_flag) + (valueBindings : Parsetree.value_binding list) : CompletionResult.t = + let completionContext = + if recFlag = Recursive then + let scopeFromBindings = + scopeValueBindings valueBindings ~scope:completionContext.scope + in + CompletionContext.withScope scopeFromBindings completionContext + else completionContext + in + valueBindings + |> Utils.findMap (fun (vb : Parsetree.value_binding) -> + completeValueBinding ~completionContext vb) + +(** Completes an expression. Designed to run without pre-checking if the cursor is in the expression. *) +and completeExpr ~completionContext (expr : Parsetree.expression) : + CompletionResult.t = + let locHasPos = completionContext.positionContext.locHasPos in + match expr.pexp_desc with + (* == VARIANTS == *) + | Pexp_construct (id, Some {pexp_desc = Pexp_tuple args; pexp_loc}) + when pexp_loc |> locHasPos -> + (* A constructor with multiple payloads, like: `Co(true, false)` or `Somepath.Co(false, true)` *) + args + |> Utils.findMapWithIndex (fun itemNum (e : Parsetree.expression) -> + completeExpr + ~completionContext: + { + completionContext with + ctxPath = + CVariantPayload + { + itemNum; + variantCtxPath = + ctxPathFromCompletionContext completionContext; + constructorName = Longident.last id.txt; + }; + } + e) + | Pexp_construct (id, Some payloadExpr) when payloadExpr.pexp_loc |> locHasPos + -> + (* A constructor with a single payload, like: `Co(true)` or `Somepath.Co(false)` *) + completeExpr + ~completionContext: + { + completionContext with + ctxPath = + CVariantPayload + { + itemNum = 0; + variantCtxPath = ctxPathFromCompletionContext completionContext; + constructorName = Longident.last id.txt; + }; + } + payloadExpr + | Pexp_construct ({txt = Lident txt; loc}, _) when loc |> locHasPos -> + (* A constructor, like: `Co` *) + CompletionResult.expression ~completionContext ~prefix:txt + | Pexp_construct (id, _) when id.loc |> locHasPos -> + (* A path, like: `Something.Co` *) + let lid = flattenLidCheckDot ~completionContext id in + CompletionResult.ctxPath (CId (lid, Module)) completionContext + (* == RECORDS == *) + | Pexp_ident {txt = Lident prefix} when Utils.hasBraces expr.pexp_attributes + -> + (* An ident with braces attribute corresponds to for example `{n}`. + Looks like a record but is parsed as an ident with braces. *) + let prefix = if prefix = "()" then "" else prefix in + let completionContext = + completionContext + |> CompletionContext.addCtxPathItem + (CRecordField + { + prefix; + seenFields = []; + recordCtxPath = ctxPathFromCompletionContext completionContext; + }) + in + CompletionResult.expression ~completionContext ~prefix + | Pexp_record ([], _) when expr.pexp_loc |> locHasPos -> + (* No fields means we're in a record body `{}` *) + let completionContext = + completionContext + |> CompletionContext.addCtxPathItem + (CRecordField + { + prefix = ""; + seenFields = []; + recordCtxPath = ctxPathFromCompletionContext completionContext; + }) + in + CompletionResult.expression ~completionContext ~prefix:"" + | Pexp_record (fields, _) when expr.pexp_loc |> locHasPos -> ( + (* A record with fields *) + let seenFields = + fields + |> List.map (fun (fieldName, _f) -> Longident.last fieldName.Location.txt) + in + let fieldToComplete = + fields + |> Utils.findMap + (fun + ((fieldName, fieldExpr) : + Longident.t Location.loc * Parsetree.expression) + -> + (* Complete regular idents *) + if locHasPos fieldName.loc then + (* Cursor in field name, complete here *) + match fieldName with + | {txt = Lident prefix} -> + CompletionResult.ctxPath + (CRecordField + { + prefix; + seenFields; + recordCtxPath = + ctxPathFromCompletionContext completionContext; + }) + completionContext + | fieldName -> + CompletionResult.ctxPath + (CId (flattenLidCheckDot ~completionContext fieldName, Value)) + completionContext + else + (* TODO: Only follow if we can follow, otherwise set context appropriately. *) + completeExpr ~completionContext fieldExpr) + in + match fieldToComplete with + | None -> ( + (* Check if there's a expr hole with an empty cursor for a field. + This means completing for an empty field `{someField: }`. *) + let fieldNameWithExprHole = + fields + |> Utils.findMap (fun (fieldName, fieldExpr) -> + if checkIfExprHoleEmptyCursor ~completionContext fieldExpr then + Some (Longident.last fieldName.Location.txt) + else None) + in + (* We found no field to complete, but we know the cursor is inside this record body. + Check if the char to the left of the cursor is ',', if so, complete for record fields.*) + match + ( fieldNameWithExprHole, + completionContext.positionContext.charBeforeNoWhitespace ) + with + | Some fieldName, _ -> + let completionContext = + completionContext + |> CompletionContext.addCtxPathItem + (CRecordFieldFollow + { + fieldName; + recordCtxPath = + ctxPathFromCompletionContext completionContext; + }) + in + CompletionResult.expression ~completionContext ~prefix:"" + | None, Some ',' -> + let completionContext = + completionContext + |> CompletionContext.addCtxPathItem + (CRecordField + { + prefix = ""; + seenFields; + recordCtxPath = + ctxPathFromCompletionContext completionContext; + }) + in + CompletionResult.expression ~completionContext ~prefix:"" + | _ -> None) + | fieldToComplete -> fieldToComplete) + (* == IDENTS == *) + | Pexp_ident lid -> + (* An identifier, like `aaa` *) + (* TODO(1) idents vs modules, etc *) + let lidPath = flattenLidCheckDot lid ~completionContext in + let last = Longident.last lid.txt in + if lid.loc |> locHasPos then + (*let completionContext = + completionContext + |> CompletionContext.addCtxPathItem (CId (lidPath, Value)) + in*) + CompletionResult.expression ~completionContext ~prefix:last + else None + | Pexp_let (recFlag, valueBindings, nextExpr) -> + (* A let binding. `let a = b` *) + let scopeFromBindings = + scopeValueBindings valueBindings ~scope:completionContext.scope + in + let completionContextWithScopeFromBindings = + completionContext |> CompletionContext.withScope scopeFromBindings + in + (* First check if the next expr is the thing with the cursor *) + if locHasPos nextExpr.pexp_loc then + completeExpr ~completionContext:completionContextWithScopeFromBindings + nextExpr + else if locHasPos expr.pexp_loc then + (* The cursor is in the expression, but not in the next expression. + Check the value bindings.*) + completeValueBindings ~recFlag ~completionContext valueBindings + else None + | Pexp_ifthenelse (condition, then_, maybeElse) -> ( + if locHasPos condition.pexp_loc then + (* TODO: I guess we could set looking for to "bool" here, since it's the if condition *) + completeExpr + ~completionContext:(CompletionContext.resetCtx completionContext) + condition + else if locHasPos then_.pexp_loc then + completeExpr + ~completionContext: + (completionContext + |> CompletionContext.setCurrentlyExpecting completionContext.ctxPath) + then_ + else + match maybeElse with + | Some else_ -> + if locHasPos else_.pexp_loc then completeExpr ~completionContext else_ + else if checkIfExprHoleEmptyCursor ~completionContext else_ then + let completionContext = + completionContext + |> CompletionContext.addCtxPathItem (CId ([], Value)) + in + CompletionResult.expression ~completionContext ~prefix:"" + else None + | _ -> + (* Check then_ too *) + if checkIfExprHoleEmptyCursor ~completionContext then_ then + let completionContext = + completionContext + |> CompletionContext.setCurrentlyExpecting completionContext.ctxPath + in + CompletionResult.expression ~completionContext ~prefix:"" + else None) + | Pexp_sequence (evalExpr, nextExpr) -> + if locHasPos evalExpr.pexp_loc then + completeExpr + ~completionContext:(CompletionContext.resetCtx completionContext) + evalExpr + else if locHasPos nextExpr.pexp_loc then + completeExpr ~completionContext nextExpr + else None + (* == Pipes == *) + | Pexp_apply + ( {pexp_desc = Pexp_ident {txt = Lident ("|." | "|.u"); loc = opLoc}}, + [ + (_, lhs); + (_, {pexp_desc = Pexp_extension _; pexp_loc = {loc_ghost = true}}); + ] ) + when locHasPos opLoc -> ( + (* Case foo-> when the parser adds a ghost expression to the rhs + so the apply expression does not include the cursor *) + match completePipe lhs ~id:"" with + | None -> None + | Some cpipe -> + completionContext |> CompletionContext.resetCtx + |> CompletionResult.ctxPath cpipe) + | Pexp_apply + ( {pexp_desc = Pexp_ident {txt = Lident ("|." | "|.u")}}, + [ + (_, lhs); + (_, {pexp_desc = Pexp_ident {txt = Longident.Lident id; loc}}); + ] ) + when locHasPos loc -> ( + (* foo->id *) + match completePipe lhs ~id with + | None -> None + | Some cpipe -> + completionContext |> CompletionContext.resetCtx + |> CompletionResult.ctxPath cpipe) + | Pexp_apply + ( {pexp_desc = Pexp_ident {txt = Lident ("|." | "|.u"); loc = opLoc}}, + [(_, lhs); _] ) + when Loc.end_ opLoc = completionContext.positionContext.cursor -> ( + match completePipe lhs ~id:"" with + | None -> None + | Some cpipe -> + completionContext |> CompletionContext.resetCtx + |> CompletionResult.ctxPath cpipe) + | Pexp_apply + ( {pexp_desc = Pexp_ident {txt = Lident ("|." | "|.u")}}, + [(_, lhs); (_, rhs)] ) -> + (* Descend into pipe parts if none of the special cases above works + but the cursor is somewhere here. *) + let completionContext = completionContext |> CompletionContext.resetCtx in + if locHasPos lhs.pexp_loc then completeExpr ~completionContext lhs + else if locHasPos rhs.pexp_loc then completeExpr ~completionContext rhs + else None + | Pexp_apply ({pexp_desc = Pexp_ident compName}, args) + when Res_parsetree_viewer.isJsxExpression expr -> ( + (* == JSX == *) + let jsxProps = CompletionJsx.extractJsxProps ~compName ~args in + let compNamePath = + flattenLidCheckDot ~completionContext ~jsx:true compName + in + let beforeCursor = completionContext.positionContext.beforeCursor in + let endPos = Loc.end_ expr.pexp_loc in + let posAfterCompName = Loc.end_ compName.loc in + let seenProps = + List.fold_right + (fun (prop : CompletionJsx.prop) seenProps -> prop.name :: seenProps) + jsxProps.props [] + in + (* Go through all of the props, looking for completions *) + let rec loop (props : CompletionJsx.prop list) = + match props with + | prop :: rest -> + if prop.posStart <= beforeCursor && beforeCursor < prop.posEnd then + (* Cursor on the prop name. *) + CompletionResult.jsx ~completionContext ~prefix:prop.name + ~pathToComponent: + (Utils.flattenLongIdent ~jsx:true jsxProps.compName.txt) + ~seenProps + else if + prop.posEnd <= beforeCursor + && beforeCursor < Loc.start prop.exp.pexp_loc + then + (* Cursor between the prop name and expr assigned. value *) + None + else if locHasPos prop.exp.pexp_loc then + (* Cursor in the expr assigned. Move into the expr and set that we're + expecting the return type of the prop. *) + let completionContext = + completionContext + |> CompletionContext.setCurrentlyExpecting + (CJsxPropValue + { + propName = prop.name; + pathToComponent = + Utils.flattenLongIdent ~jsx:true jsxProps.compName.txt; + }) + in + completeExpr ~completionContext prop.exp + else if + locHasPos expr.pexp_loc + && checkIfExprHoleEmptyCursor ~completionContext prop.exp + then + (* Cursor is in the expression, but on an empty assignment. *) + let completionContext = + completionContext + |> CompletionContext.setCurrentlyExpecting + (CJsxPropValue + { + propName = prop.name; + pathToComponent = + Utils.flattenLongIdent ~jsx:true jsxProps.compName.txt; + }) + in + CompletionResult.expression ~completionContext ~prefix:"" + else loop rest + | [] -> + let beforeChildrenStart = + match jsxProps.childrenStart with + | Some childrenPos -> beforeCursor < childrenPos + | None -> beforeCursor <= endPos + in + let afterCompName = beforeCursor >= posAfterCompName in + if afterCompName && beforeChildrenStart then + CompletionResult.jsx ~completionContext ~prefix:"" + ~pathToComponent: + (Utils.flattenLongIdent ~jsx:true jsxProps.compName.txt) + ~seenProps + else None + in + let jsxCompletable = loop jsxProps.props in + match jsxCompletable with + | Some jsxCompletable -> Some jsxCompletable + | None -> + if locHasPos compName.loc then + (* The component name has the cursor. + Check if this is a HTML element (lowercase initial char) or a component (uppercase initial char). *) + match compNamePath with + | [prefix] when Char.lowercase_ascii prefix.[0] = prefix.[0] -> + CompletionResult.htmlElement ~completionContext ~prefix + | _ -> + CompletionResult.ctxPath + (CId (compNamePath, Module)) + (completionContext |> CompletionContext.resetCtx) + else None) + | Pexp_apply (fnExpr, _args) when locHasPos fnExpr.pexp_loc -> + (* Handle when the cursor is in the function expression itself. *) + fnExpr + |> completeExpr + ~completionContext:(completionContext |> CompletionContext.resetCtx) + | Pexp_apply (fnExpr, args) -> ( + (* Handle when the cursor isn't in the function expression. Possibly in an argument. *) + (* TODO: Are we moving into all expressions we need here? The fn expression itself? *) + let fnContextPath = exprToContextPath fnExpr in + match fnContextPath with + | None -> None + | Some functionContextPath -> ( + let beforeCursor = completionContext.positionContext.beforeCursor in + let isPipedExpr = false (* TODO: Implement *) in + let args = extractExpApplyArgs ~args in + let endPos = Loc.end_ expr.pexp_loc in + let posAfterFnExpr = Loc.end_ fnExpr.pexp_loc in + let fnHasCursor = + posAfterFnExpr <= beforeCursor && beforeCursor < endPos + in + (* All of the labels already written in the application. *) + let seenLabels = + List.fold_right + (fun arg seenLabels -> + match arg with + | {label = Some labelled} -> labelled.name :: seenLabels + | {label = None} -> seenLabels) + args [] + in + let makeCompletionContextWithArgumentLabel argumentLabel + ~functionContextPath = + completionContext |> CompletionContext.resetCtx + |> CompletionContext.currentlyExpectingOrReset + (Some (CFunctionArgument {functionContextPath; argumentLabel})) + in + (* Piped expressions always have an initial unlabelled argument. *) + let unlabelledCount = ref (if isPipedExpr then 1 else 0) in + let rec loop args = + match args with + | {label = Some labelled; exp} :: rest -> + if labelled.posStart <= beforeCursor && beforeCursor < labelled.posEnd + then + (* Complete for a label: `someFn(~labelNam)` *) + CompletionResult.namedArg ~completionContext ~prefix:labelled.name + ~seenLabels ~functionContextPath + else if locHasPos exp.pexp_loc then + (* Completing in the assignment of labelled argument, with a value. + `someFn(~someLabel=someIden)` *) + let completionContext = + makeCompletionContextWithArgumentLabel (Labelled labelled.name) + ~functionContextPath + in + completeExpr ~completionContext exp + else if CompletionExpressions.isExprHole exp then + (* Completing in the assignment of labelled argument, with no value yet. + The parser inserts an expr hole. `someFn(~someLabel=)` *) + let completionContext = + makeCompletionContextWithArgumentLabel (Labelled labelled.name) + ~functionContextPath + in + CompletionResult.expression ~completionContext ~prefix:"" + else loop rest + | {label = None; exp} :: rest -> + if Res_parsetree_viewer.isTemplateLiteral exp then + (* Ignore template literals, or we mess up completion inside of them. *) + None + else if locHasPos exp.pexp_loc then + (* Completing in an unlabelled argument with a value. `someFn(someV) *) + let completionContext = + makeCompletionContextWithArgumentLabel + (Unlabelled {argumentPosition = !unlabelledCount}) + ~functionContextPath + in + completeExpr ~completionContext exp + else if CompletionExpressions.isExprHole exp then + (* Completing in an unlabelled argument without a value. `someFn(true, ) *) + let completionContext = + makeCompletionContextWithArgumentLabel + (Unlabelled {argumentPosition = !unlabelledCount}) + ~functionContextPath + in + CompletionResult.expression ~completionContext ~prefix:"" + else ( + unlabelledCount := !unlabelledCount + 1; + loop rest) + | [] -> + if fnHasCursor then + (* No matches, but we know we have the cursor. Check the first char + behind the cursor. '~' means label completion. *) + match completionContext.positionContext.charBeforeCursor with + | Some '~' -> + CompletionResult.namedArg ~completionContext ~prefix:"" + ~seenLabels ~functionContextPath + | _ -> + (* No '~'. Assume we want to complete for the next unlabelled argument. *) + let completionContext = + makeCompletionContextWithArgumentLabel + (Unlabelled {argumentPosition = !unlabelledCount}) + ~functionContextPath + in + CompletionResult.expression ~completionContext ~prefix:"" + else None + in + match args with + (* Special handling for empty fn calls, e.g. `let _ = someFn()` *) + | [ + { + label = None; + exp = {pexp_desc = Pexp_construct ({txt = Lident "()"}, _)}; + }; + ] + when fnHasCursor -> + let completionContext = + makeCompletionContextWithArgumentLabel + (Unlabelled {argumentPosition = 0}) + ~functionContextPath + in + CompletionResult.expression ~completionContext ~prefix:"" + | _ -> loop args)) + | Pexp_fun _ -> + (* We've found a function definition, like `let whatever = (someStr: string) => {}` *) + let rec loopFnExprs ~(completionContext : CompletionContext.t) + (expr : Parsetree.expression) = + (* TODO: Handle completing in default expressions and patterns *) + match expr.pexp_desc with + | Pexp_fun (_arg, _defaultExpr, pattern, nextExpr) -> + let scopeFromPattern = + scopePattern ~scope:completionContext.scope pattern + in + loopFnExprs + ~completionContext: + (completionContext |> CompletionContext.withScope scopeFromPattern) + nextExpr + | Pexp_constraint (expr, typ) -> + (expr, completionContext, ctxPathFromCoreType ~completionContext typ) + | _ -> (expr, completionContext, None) + in + let expr, completionContext, fnReturnConstraint = + loopFnExprs ~completionContext expr + in + (* Set the expected type correctly for the expr body *) + let completionContext = + match fnReturnConstraint with + | None -> + (* Having a Type here already means the binding itself had a constraint on it. Since we're now moving into the function body, + we'll need to ensure it's the function return type we use for completion, not the function type itself *) + completionContext + |> CompletionContext.setCurrentlyExpecting + (CFunctionReturnType + {functionCtxPath = completionContext.currentlyExpecting}) + | Some ctxPath -> + completionContext |> CompletionContext.setCurrentlyExpecting ctxPath + in + if locHasPos expr.pexp_loc then completeExpr ~completionContext expr + else if checkIfExprHoleEmptyCursor ~completionContext expr then + let completionContext = + completionContext |> CompletionContext.addCtxPathItem (CId ([], Value)) + in + CompletionResult.expression ~completionContext ~prefix:"" + else None + | Pexp_match _ | Pexp_unreachable | Pexp_constant _ | Pexp_function _ + | Pexp_try (_, _) + | Pexp_tuple _ + | Pexp_construct (_, _) + | Pexp_variant (_, _) + | Pexp_record (_, _) + | Pexp_field (_, _) + | Pexp_setfield (_, _, _) + | Pexp_array _ + | Pexp_while (_, _) + | Pexp_for (_, _, _, _, _) + | Pexp_constraint (_, _) + | Pexp_coerce (_, _, _) + | Pexp_send (_, _) + | Pexp_setinstvar (_, _) + | Pexp_override _ + | Pexp_letmodule (_, _, _) + | Pexp_letexception (_, _) + | Pexp_assert _ | Pexp_lazy _ + | Pexp_poly (_, _) + | Pexp_newtype (_, _) + | Pexp_pack _ + | Pexp_open (_, _, _) + | Pexp_extension _ -> + None + | Pexp_object _ | Pexp_new _ -> (* These are irrelevant to ReScript *) None + +and completePattern ~(completionContext : CompletionContext.t) + (pat : Parsetree.pattern) : CompletionResult.t = + let locHasPos = completionContext.positionContext.locHasPos in + match pat.ppat_desc with + | Ppat_lazy p + | Ppat_constraint (p, _) + | Ppat_alias (p, _) + | Ppat_exception p + | Ppat_open (_, p) -> + (* Can just continue into these patterns. *) + if locHasPos pat.ppat_loc then p |> completePattern ~completionContext + else None + | Ppat_or (p1, p2) -> ( + (* Try to complete each `or` pattern *) + let orPatCompleted = + [p1; p2] + |> List.find_map (fun p -> + if locHasPos p.Parsetree.ppat_loc then + completePattern ~completionContext p + else None) + in + match orPatCompleted with + | None + when CompletionPatterns.isPatternHole p1 + || CompletionPatterns.isPatternHole p2 -> + (* TODO(1) explain this *) + CompletionResult.pattern ~completionContext ~prefix:"" + | res -> res) + | Ppat_var {txt; loc} -> + (* A variable, like `{ someThing: someV}*) + if locHasPos loc then + CompletionResult.pattern ~completionContext ~prefix:txt + else None + | Ppat_record ([], _) -> + (* Empty fields means we're in a record body `{}`. Complete for the fields. *) + if locHasPos pat.ppat_loc then + let completionContext = + CompletionContext.addCtxPathItem + (CRecordField + { + seenFields = []; + prefix = ""; + recordCtxPath = ctxPathFromCompletionContext completionContext; + }) + completionContext + in + CompletionResult.pattern ~completionContext ~prefix:"" + else None + | Ppat_record (fields, _) -> ( + (* Record body with fields, where we know the cursor is inside of the record body somewhere. *) + let seenFields = + fields + |> List.filter_map (fun (fieldName, _f) -> + match fieldName with + | {Location.txt = Longident.Lident fieldName} -> Some fieldName + | _ -> None) + in + let fieldNameWithCursor = + fields + |> List.find_map + (fun ((fieldName : Longident.t Location.loc), _fieldPattern) -> + if locHasPos fieldName.Location.loc then Some fieldName else None) + in + let fieldPatternWithCursor = + fields + |> List.find_map (fun (fieldName, fieldPattern) -> + if locHasPos fieldPattern.Parsetree.ppat_loc then + Some (fieldName, fieldPattern) + else None) + in + match (fieldNameWithCursor, fieldPatternWithCursor) with + | Some fieldName, _ -> + (* {someFieldName: someValue} *) + let prefix = Longident.last fieldName.txt in + CompletionResult.pattern ~prefix + ~completionContext: + (CompletionContext.addCtxPathItem + (CRecordField + { + seenFields; + prefix; + recordCtxPath = ctxPathFromCompletionContext completionContext; + }) + completionContext) + | None, Some (fieldName, fieldPattern) -> + (* {someFieldName: someOtherPattern} *) + let prefix = Longident.last fieldName.txt in + let completionContext = + CompletionContext.addCtxPathItem + (CRecordField + { + seenFields; + prefix; + recordCtxPath = ctxPathFromCompletionContext completionContext; + }) + completionContext + in + completePattern ~completionContext fieldPattern + | None, None -> + if locHasPos pat.ppat_loc then + (* We know the cursor is here, but it's not in a field name nor a field pattern. + Check empty field patterns. TODO(1) *) + None + else None) + | Ppat_tuple tupleItems -> ( + let tupleItemWithCursor = + tupleItems + |> Utils.findMapWithIndex (fun index (tupleItem : Parsetree.pattern) -> + if locHasPos tupleItem.ppat_loc then Some (index, tupleItem) + else None) + in + match tupleItemWithCursor with + | Some (itemNum, tupleItem) -> + let completionContext = + completionContext + |> CompletionContext.addCtxPathItem + (CTupleItem + { + itemNum; + tupleCtxPath = ctxPathFromCompletionContext completionContext; + }) + in + completePattern ~completionContext tupleItem + | None -> + if locHasPos pat.ppat_loc then + (* We found no tuple item with the cursor, but we know the cursor is in the + pattern. Check if the user is trying to complete an empty tuple item *) + match completionContext.positionContext.charBeforeNoWhitespace with + | Some ',' -> + (* `(true, false, )` itemNum = 2, or `(true, , false)` itemNum = 1 *) + (* Figure out which tuple item is active. *) + let itemNum = ref (-1) in + tupleItems + |> List.iteri (fun index (pat : Parsetree.pattern) -> + if + completionContext.positionContext.beforeCursor + >= Loc.start pat.ppat_loc + then itemNum := index); + if !itemNum > -1 then + let completionContext = + completionContext + |> CompletionContext.addCtxPathItem + (CTupleItem + { + itemNum = !itemNum + 1; + tupleCtxPath = + ctxPathFromCompletionContext completionContext; + }) + in + CompletionResult.pattern ~completionContext ~prefix:"" + else None + | Some '(' -> + (* TODO: This should work (start of tuple), but the parser is broken for this case: + let ( , true) = someRecordVar. If we fix that completing in the first position + could work too. *) + let completionContext = + completionContext + |> CompletionContext.addCtxPathItem + (CTupleItem + { + itemNum = 0; + tupleCtxPath = + ctxPathFromCompletionContext completionContext; + }) + in + CompletionResult.pattern ~completionContext ~prefix:"" + | _ -> None + else None) + | Ppat_array items -> + if locHasPos pat.ppat_loc then + if List.length items = 0 then + (* {someArr: []} *) + let completionContext = + completionContext |> CompletionContext.addCtxPathItem (CArray None) + in + CompletionResult.pattern ~completionContext ~prefix:"" + else + let arrayItemWithCursor = + items + |> List.find_opt (fun (item : Parsetree.pattern) -> + locHasPos item.ppat_loc) + in + match + ( arrayItemWithCursor, + completionContext.positionContext.charBeforeNoWhitespace ) + with + | Some item, _ -> + (* Found an array item with the cursor. *) + let completionContext = + completionContext |> CompletionContext.addCtxPathItem (CArray None) + in + completePattern ~completionContext item + | None, Some ',' -> + (* No array item with the cursor, but we know the cursor is in the pattern. + Check for "," which would signify the user is looking to add another + array item to the pattern. *) + let completionContext = + completionContext |> CompletionContext.addCtxPathItem (CArray None) + in + CompletionResult.pattern ~completionContext ~prefix:"" + | _ -> None + else None + | Ppat_any -> + (* We treat any `_` as an empty completion. This is mainly because we're + inserting `_` in snippets and automatically put the cursor there. So + letting it trigger an empty completion improves the ergonomics by a + lot. *) + if locHasPos pat.ppat_loc then + CompletionResult.pattern ~completionContext ~prefix:"" + else None + | Ppat_construct (_, _) + | Ppat_variant (_, _) + | Ppat_type _ | Ppat_unpack _ | Ppat_extension _ | Ppat_constant _ + | Ppat_interval _ -> + None + +let completion ~currentFile ~path ~debug ~offset ~posCursor text = + let positionContext = PositionContext.make ~offset ~posCursor text in + let completionContext = CompletionContext.make positionContext in + if Filename.check_suffix path ".res" then + let parser = + Res_driver.parsingEngine.parseImplementation ~forPrinter:false + in + let {Res_driver.parsetree = str} = parser ~filename:currentFile in + str |> completeFromStructure ~completionContext + else if Filename.check_suffix path ".resi" then + let parser = Res_driver.parsingEngine.parseInterface ~forPrinter:false in + let {Res_driver.parsetree = signature} = parser ~filename:currentFile in + None + else None diff --git a/analysis/src/CompletionNewTypes.ml b/analysis/src/CompletionNewTypes.ml new file mode 100644 index 000000000..47864be86 --- /dev/null +++ b/analysis/src/CompletionNewTypes.ml @@ -0,0 +1,243 @@ +open SharedTypes +open CompletionsNewTypesCtxPath + +module PositionContext = struct + type t = { + offset: int; (** The offset *) + cursor: Pos.t; (** The actual position of the cursor *) + beforeCursor: Pos.t; (** The position just before the cursor *) + noWhitespace: Pos.t; + (** The position of the cursor, removing any whitespace _before_ it *) + charBeforeNoWhitespace: char option; + (** The first character before the cursor, excluding any whitespace *) + charBeforeCursor: char option; + (** The char before the cursor, not excluding whitespace *) + whitespaceAfterCursor: char option; + (** The type of whitespace after the cursor, if any *) + locHasPos: Location.t -> bool; + (** A helper for checking whether a loc has the cursor (beforeCursor). + This is the most natural position to check when figuring out if the user has the cursor in something. *) + } + + let make ~offset ~posCursor text = + let offsetNoWhite = Utils.skipWhite text (offset - 1) in + let posNoWhite = + let line, col = posCursor in + (line, max 0 col - offset + offsetNoWhite) + in + let firstCharBeforeCursorNoWhite = + if offsetNoWhite < String.length text && offsetNoWhite >= 0 then + Some text.[offsetNoWhite] + else None + in + let posBeforeCursor = Pos.posBeforeCursor posCursor in + let charBeforeCursor, whitespaceAfterCursor = + match Pos.positionToOffset text posCursor with + | Some offset when offset > 0 -> ( + let charBeforeCursor = text.[offset - 1] in + let charAtCursor = + if offset < String.length text then text.[offset] else '\n' + in + match charAtCursor with + | ' ' | '\t' | '\r' | '\n' -> + (Some charBeforeCursor, Some charBeforeCursor) + | _ -> (Some charBeforeCursor, None)) + | _ -> (None, None) + in + let locHasPos loc = + loc |> CursorPosition.locHasCursor ~pos:posBeforeCursor + in + { + offset; + beforeCursor = posBeforeCursor; + noWhitespace = posNoWhite; + charBeforeNoWhitespace = firstCharBeforeCursorNoWhite; + cursor = posCursor; + charBeforeCursor; + whitespaceAfterCursor; + locHasPos; + } +end + +module CompletionContext = struct + type t = { + positionContext: PositionContext.t; + scope: Scope.t; + currentlyExpecting: ctxPath; + ctxPath: ctxPath; + } + + let make positionContext = + { + positionContext; + scope = Scope.create (); + currentlyExpecting = CNone; + ctxPath = CNone; + } + + let resetCtx completionContext = + {completionContext with currentlyExpecting = CNone; ctxPath = CNone} + + let withScope scope completionContext = {completionContext with scope} + + let setCurrentlyExpecting currentlyExpecting completionContext = + {completionContext with currentlyExpecting} + + let currentlyExpectingOrReset currentlyExpecting completionContext = + match currentlyExpecting with + | None -> {completionContext with currentlyExpecting = CNone} + | Some currentlyExpecting -> {completionContext with currentlyExpecting} + + let currentlyExpectingOrTypeAtLoc ~loc currentlyExpecting completionContext = + match currentlyExpecting with + | None -> {completionContext with currentlyExpecting = CTypeAtLoc loc} + | Some currentlyExpecting -> {completionContext with currentlyExpecting} + + let currentlyExpectingOrTypeAtLoc2 ~loc currentlyExpecting completionContext = + let currentlyExpecting = + match currentlyExpecting with + | None -> CTypeAtLoc loc + | Some currentlyExpecting -> currentlyExpecting + in + {completionContext with currentlyExpecting; ctxPath = currentlyExpecting} + + let withResetCurrentlyExpecting completionContext = + {completionContext with currentlyExpecting = CNone} + + let addCtxPathItem ctxPath completionContext = + {completionContext with ctxPath} +end + +module CompletionInstruction = struct + (** This is the completion instruction, that's responsible for resolving something at + context path X *) + type t = + | Cnone + | CtxPath of ctxPath + | Cpattern of { + ctxPath: ctxPath; + (** This is the context path inside of the pattern itself. + Used to walk up to the type we're looking to complete. *) + rootType: ctxPath; + (** This is the an instruction to find where completion starts + from. If we're completing inside of a record, it should resolve + to the record itself. *) + prefix: string; + } (** Completing inside of a pattern. *) + | Cexpression of { + ctxPath: ctxPath; + (** This is the context path inside of the expression itself. + Used to walk up to the type we're looking to complete. *) + rootType: ctxPath; + (** This is the an instruction to find where completion starts + from. If we're completing inside of a record, it should resolve + to the record itself. *) + prefix: string; + } (** Completing inside of an expression. *) + | CnamedArg of { + ctxPath: ctxPath; + (** Context path to the function with the argument. *) + seenLabels: string list; + (** All the already seen labels in the function call. *) + prefix: string; (** The text the user has written so far.*) + } + | Cjsx of { + pathToComponent: string list; + (** The path to the component: `["M", "Comp"]`. *) + prefix: string; (** What the user has already written. `"id"`. *) + seenProps: string list; + (** A list of all of the props that has already been entered.*) + } + | ChtmlElement of {prefix: string (** What the user has written so far. *)} + (** Completing for a regular HTML element. *) + + let ctxPath ctxPath = CtxPath ctxPath + + let pattern ~(completionContext : CompletionContext.t) ~prefix = + Cpattern + { + prefix; + rootType = completionContext.currentlyExpecting; + ctxPath = completionContext.ctxPath; + } + + let expression ~(completionContext : CompletionContext.t) ~prefix = + Cexpression + { + prefix; + rootType = completionContext.currentlyExpecting; + ctxPath = completionContext.ctxPath; + } + + let namedArg ~prefix ~functionContextPath ~seenLabels = + CnamedArg {prefix; ctxPath = functionContextPath; seenLabels} + + let jsx ~prefix ~pathToComponent ~seenProps = + Cjsx {prefix; pathToComponent; seenProps} + + let htmlElement ~prefix = ChtmlElement {prefix} + + let toString (c : t) = + match c with + | Cnone -> "Cnone" + | CtxPath ctxPath -> Printf.sprintf "CtxPath: %s" (ctxPathToString ctxPath) + | Cpattern {ctxPath; prefix} -> + Printf.sprintf "Cpattern: ctxPath: %s, %s" (ctxPathToString ctxPath) + (match prefix with + | "" -> "" + | prefix -> Printf.sprintf ", prefix: \"%s\"" prefix) + | Cexpression {ctxPath; prefix} -> + Printf.sprintf "Cexpression: ctxPath: %s %s" (ctxPathToString ctxPath) + (match prefix with + | "" -> "" + | prefix -> Printf.sprintf ", prefix: \"%s\"" prefix) + | CnamedArg {prefix; ctxPath; seenLabels} -> + "CnamedArg(" + ^ (ctxPath |> ctxPathToString) + ^ ", " ^ str prefix ^ ", " ^ (seenLabels |> list) ^ ")" + | Cjsx {prefix; pathToComponent; seenProps} -> + "Cjsx(" ^ (pathToComponent |> ident) ^ ", " ^ str prefix ^ ", " + ^ (seenProps |> list) ^ ")" + | ChtmlElement {prefix} -> "ChtmlElement <" ^ prefix ^ " />" +end + +module CompletionResult = struct + type t = (CompletionInstruction.t * CompletionContext.t) option + + let make (instruction : CompletionInstruction.t) + (context : CompletionContext.t) = + Some (instruction, context) + + let ctxPath (ctxPath : ctxPath) (completionContext : CompletionContext.t) = + let completionContext = + completionContext |> CompletionContext.addCtxPathItem ctxPath + in + make + (CompletionInstruction.ctxPath completionContext.ctxPath) + completionContext + + let pattern ~(completionContext : CompletionContext.t) ~prefix = + make + (CompletionInstruction.pattern ~completionContext ~prefix) + completionContext + + let expression ~(completionContext : CompletionContext.t) ~prefix = + make + (CompletionInstruction.expression ~completionContext ~prefix) + completionContext + + let namedArg ~(completionContext : CompletionContext.t) ~prefix ~seenLabels + ~functionContextPath = + make + (CompletionInstruction.namedArg ~functionContextPath ~prefix ~seenLabels) + completionContext + + let jsx ~(completionContext : CompletionContext.t) ~prefix ~pathToComponent + ~seenProps = + make + (CompletionInstruction.jsx ~prefix ~pathToComponent ~seenProps) + completionContext + + let htmlElement ~(completionContext : CompletionContext.t) ~prefix = + make (CompletionInstruction.htmlElement ~prefix) completionContext +end diff --git a/analysis/src/Completions.ml b/analysis/src/Completions.ml index 42176bb3b..a419f4507 100644 --- a/analysis/src/Completions.ml +++ b/analysis/src/Completions.ml @@ -20,3 +20,31 @@ let getCompletions ~debug ~path ~pos ~currentFile ~forHover = ~forHover in Some (completables, full, scope))) + +let getCompletions2 ~debug ~path ~pos ~currentFile ~forHover = + let textOpt = Files.readFile currentFile in + match textOpt with + | None | Some "" -> None + | Some text -> ( + match Pos.positionToOffset text pos with + | None -> None + | Some offset -> ( + match + CompletionFrontEndNew.completion ~offset ~debug ~path ~posCursor:pos + ~currentFile text + with + | None -> + print_endline "No completions"; + None + | Some (res, ctx) -> ( + Printf.printf "Scope: %i items\n" (List.length ctx.scope); + match Cmt.loadFullCmtFromPath ~path with + | None -> None + | Some full -> + let env = SharedTypes.QueryEnv.fromFile full.file in + let completables = + res + |> CompletionBackendNew.processCompletable ~debug ~full ~pos + ~scope:ctx.scope ~env ~forHover + in + Some (completables, full, ctx.scope)))) diff --git a/analysis/src/CompletionsNewTypesCtxPath.ml b/analysis/src/CompletionsNewTypesCtxPath.ml new file mode 100644 index 000000000..a4f514db9 --- /dev/null +++ b/analysis/src/CompletionsNewTypesCtxPath.ml @@ -0,0 +1,147 @@ +type completionCategory = Type | Value | Module | Field + +type argumentLabel = + | Unlabelled of {argumentPosition: int} + | Labelled of string + | Optional of string + +type ctxPath = + | CNone (** Nothing. *) + | CUnknown (** Something that cannot be resolved right now *) + | CId of string list * completionCategory + (** A regular id of an expected category. `let fff = thisIsAnId` and `let fff = SomePath.alsoAnId` *) + | CVariantPayload of { + variantCtxPath: ctxPath; + itemNum: int; + constructorName: string; + } + (** A variant payload. `Some()` = itemNum 0, `Whatever(true, f)` = itemNum 1 *) + | CTupleItem of {tupleCtxPath: ctxPath; itemNum: int} + (** A tuple item. `(true, false, )` = itemNum 2 *) + | CRecordField of { + recordCtxPath: ctxPath; + seenFields: string list; + prefix: string; + } + (** A record field. `let f = {on: true, s}` seenFields = [on], prefix = "s",*) + (* TODO: Can we merge with CRecordFieldAccess?*) + | CRecordFieldFollow of {recordCtxPath: ctxPath; fieldName: string} + (** Follow this record field. {}*) + | COption of ctxPath (** An option with an inner type. *) + | CArray of ctxPath option (** An array with an inner type. *) + | CTuple of ctxPath list (** A tuple. *) + | CBool + | CString + | CInt + | CFloat + | CRecordBody of {recordCtxPath: ctxPath; seenFields: string list} + | CAwait of ctxPath (** Awaiting a function call. *) + | CFunction of {returnType: ctxPath} (** A function *) + | CRecordFieldAccess of {recordCtxPath: ctxPath; fieldName: string} + (** Field access. `whateverVariable.fieldName`. The ctxPath points to the value of `whateverVariable`, + and the string is the name of the field we're accessing. *) + | CObj of {objectCtxPath: ctxPath; propertyName: string} + (** Object property access. `whateverVariable["fieldName"]`. The ctxPath points to the value of `whateverVariable`, + and the string is the name of the property we're accessing. *) + | CApply of {functionCtxPath: ctxPath; args: Asttypes.arg_label list} + (** Function application. `someFunction(someVar, ~otherLabel="hello")`. The ctxPath points to the function. *) + | CFunctionArgument of { + functionContextPath: ctxPath; + argumentLabel: argumentLabel; + } (** A function argument, either labelled or unlabelled.*) + | CPipe of { + functionCtxPath: ctxPath; + (** Context path to the function being called. *) + id: string; + lhsLoc: Location.t; (** Location of the left hand side. *) + } (** Piped call. `foo->someFn`. *) + | CJsxPropValue of { + pathToComponent: string list; + (** The path to the component this property is from. *) + propName: string; (** The prop name we're going through. *) + } (** A JSX property. *) + | CTypeAtLoc of Location.t (** A type at a location. *) + | CFunctionReturnType of {functionCtxPath: ctxPath} + (** An instruction to resolve the return type of the type at the + provided context path, if it's a function (it should always be, + but you know...) *) + +let str s = if s = "" then "\"\"" else s +let list l = "[" ^ (l |> List.map str |> String.concat ", ") ^ "]" +let ident l = l |> List.map str |> String.concat "." + +let rec ctxPathToString (ctxPath : ctxPath) = + match ctxPath with + | CUnknown -> "CUnknown" + | CNone -> "CUnknown" + | CBool -> "bool" + | CFloat -> "float" + | CInt -> "int" + | CString -> "string" + | CJsxPropValue {pathToComponent; propName} -> + "<" ^ (pathToComponent |> list) ^ " =" ^ propName ^ "/>" + | CAwait ctxPath -> Printf.sprintf "await (%s)" (ctxPathToString ctxPath) + | CApply {functionCtxPath; args} -> + Printf.sprintf "%s(%s)" + (ctxPathToString functionCtxPath) + (args + |> List.map (function + | Asttypes.Nolabel -> "Nolabel" + | Labelled s -> "~" ^ s + | Optional s -> "?" ^ s) + |> String.concat ", ") + | CRecordFieldAccess {recordCtxPath; fieldName} -> + Printf.sprintf "(%s).%s" (ctxPathToString recordCtxPath) fieldName + | CObj {objectCtxPath; propertyName} -> + Printf.sprintf "(%s)[\"%s\"]" (ctxPathToString objectCtxPath) propertyName + | CFunction {returnType} -> + Printf.sprintf "() => %s" (ctxPathToString returnType) + | CTuple ctxPaths -> + Printf.sprintf "tuple(%s)" + (ctxPaths |> List.map ctxPathToString |> String.concat ", ") + | CId (prefix, typ) -> + Printf.sprintf "%s(prefix=%s)" + (match typ with + | Value -> "Value" + | Type -> "Type" + | Module -> "Module" + | Field -> "Field") + (ident prefix) + | CVariantPayload {variantCtxPath; itemNum; constructorName} -> + Printf.sprintf "(%s)->variantPayload(%s<$%i>)" + (ctxPathToString variantCtxPath) + constructorName itemNum + | CTupleItem {tupleCtxPath; itemNum} -> + Printf.sprintf "%s->tupleItem($%i)" (ctxPathToString tupleCtxPath) itemNum + | CRecordField {recordCtxPath; prefix; seenFields} -> + Printf.sprintf "%s->recordField(\"%s\", %s)" + (ctxPathToString recordCtxPath) + prefix (seenFields |> list) + | CRecordBody {recordCtxPath; seenFields} -> + Printf.sprintf "%s->recordBody(%s)" + (ctxPathToString recordCtxPath) + (seenFields |> list) + | COption ctxPath -> Printf.sprintf "option<%s>" (ctxPathToString ctxPath) + | CArray ctxPath -> + Printf.sprintf "array%s" + (match ctxPath with + | None -> "" + | Some ctxPath -> "<" ^ ctxPathToString ctxPath ^ ">") + | CFunctionArgument {functionContextPath; argumentLabel} -> + "functionArgument(" + ^ (functionContextPath |> ctxPathToString) + ^ ")(" + ^ (match argumentLabel with + | Unlabelled {argumentPosition} -> "$" ^ string_of_int argumentPosition + | Labelled name -> "~" ^ name + | Optional name -> "~" ^ name ^ "=?") + ^ ")" + | CPipe {functionCtxPath; id} -> + "pipe(" ^ ctxPathToString functionCtxPath ^ ")->" ^ id + | CRecordFieldFollow {fieldName; recordCtxPath} -> + Printf.sprintf "(%s)->followRecordField{%s}" + (ctxPathToString recordCtxPath) + fieldName + | CTypeAtLoc loc -> Printf.sprintf "CTypeAtLoc: %s" (Loc.toString loc) + | CFunctionReturnType {functionCtxPath} -> + Printf.sprintf "returnTypeOf(%s)" (ctxPathToString functionCtxPath) diff --git a/analysis/src/Scope.ml b/analysis/src/Scope.ml index 5251486f8..aa0257f91 100644 --- a/analysis/src/Scope.ml +++ b/analysis/src/Scope.ml @@ -1,10 +1,14 @@ +type ctxPathType = + | Completable of SharedTypes.Completable.contextPath + | New of CompletionsNewTypesCtxPath.ctxPath + type item = | Constructor of string * Location.t | Field of string * Location.t | Module of string * Location.t | Open of string list | Type of string * Location.t - | Value of string * Location.t * SharedTypes.Completable.contextPath option + | Value of string * Location.t * ctxPathType option type t = item list @@ -30,10 +34,14 @@ let addValue ~name ~loc ?contextPath x = (if showDebug then match contextPath with | None -> Printf.printf "adding value '%s', no ctxPath\n" name - | Some contextPath -> + | Some (Completable contextPath) -> + if showDebug then + Printf.printf "adding value '%s' with ctxPath: %s\n" name + (SharedTypes.Completable.contextPathToString contextPath) + | Some (New contextPath) -> if showDebug then Printf.printf "adding value '%s' with ctxPath: %s\n" name - (SharedTypes.Completable.contextPathToString contextPath)); + (CompletionsNewTypesCtxPath.ctxPathToString contextPath)); Value (name, loc, contextPath) :: x let addType ~name ~loc x = Type (name, loc) :: x diff --git a/analysis/src/SharedTypes.ml b/analysis/src/SharedTypes.ml index 7ee60c8e8..e4bf6f692 100644 --- a/analysis/src/SharedTypes.ml +++ b/analysis/src/SharedTypes.ml @@ -733,7 +733,9 @@ module Completion = struct | FileModule of string | Snippet of string | ExtractedType of completionType * [`Value | `Type] - | FollowContextPath of Completable.contextPath + | FollowContextPath of + [ `Completable of Completable.contextPath + | `New of CompletionsNewTypesCtxPath.ctxPath ] type t = { name: string; diff --git a/analysis/src/TypeUtils.ml b/analysis/src/TypeUtils.ml index a0ea3210a..34f4c3ce3 100644 --- a/analysis/src/TypeUtils.ml +++ b/analysis/src/TypeUtils.ml @@ -126,7 +126,7 @@ let rec extractType ~env ~package (t : Types.type_expr) = (* Uncurried functions. *) match extractFunctionType t ~env ~package with | args, tRet when args <> [] -> - Some (Tfunction {env; args; typ = t; uncurried = true; returnType = tRet}) + Some (Tfunction {env; args; typ = t; returnType = tRet; uncurried = true}) | _args, _tRet -> None) | Tconstr (path, typeArgs, _) -> ( match References.digConstructor ~env ~package path with diff --git a/analysis/src/Utils.ml b/analysis/src/Utils.ml index e548457f2..5078d8b7d 100644 --- a/analysis/src/Utils.ml +++ b/analysis/src/Utils.ml @@ -216,3 +216,23 @@ let rec lastElements list = let lowercaseFirstChar s = if String.length s = 0 then s else String.mapi (fun i c -> if i = 0 then Char.lowercase_ascii c else c) s + +let findMap f lst = + let rec aux f = function + | [] -> None + | x :: xs -> ( + match f x with + | None -> aux f xs + | Some _ as result -> result) + in + aux f lst + +let findMapWithIndex f lst = + let rec aux f index = function + | [] -> None + | x :: xs -> ( + match f index x with + | None -> aux f (index + 1) xs + | Some _ as result -> result) + in + aux f 0 lst diff --git a/analysis/src/new-completions-todo.md b/analysis/src/new-completions-todo.md new file mode 100644 index 000000000..78b6f72f6 --- /dev/null +++ b/analysis/src/new-completions-todo.md @@ -0,0 +1,14 @@ +## Questions + +- Figure out location for completeExpr - when do we need to check locs, and when not? +- Pipe PPX transform, should we run it ourselves in the editor tooling? +- Is there a practical difference btween Cexpression and Cpath? + +### Ideas + +- Scope JSX completions to things we know are components? +- Extra context: Now in a React component function, etc + +### Bugs + +- Empty JSX prop completion not triggering diff --git a/analysis/tests/src/CompletionNew.res b/analysis/tests/src/CompletionNew.res new file mode 100644 index 000000000..fda377a14 --- /dev/null +++ b/analysis/tests/src/CompletionNew.res @@ -0,0 +1,163 @@ +let myVar = true + +// let myFunc = m +// ^co2 + +type rec someVariant = One | Two | Three(bool, option) + +// let myFunc: someVariant = O +// ^co2 + +// let myFunc: someVariant = Three(t) +// ^co2 + +// let myFunc: someVariant = Three(true, So) +// ^co2 + +// let myFunc: someVariant = Three(true, Some(O)) +// ^co2 + +type nestedRecord = { + on: bool, + off?: bool, + maybeVariant?: someVariant, +} + +type someRecord = {nested: option, variant: someVariant, someString: string} + +// let myFunc: someRecord = {} +// ^co2 + +// let myFunc: someRecord = {n} +// ^co2 + +// let myFunc: someRecord = {variant: O} +// ^co2 + +// let myFunc: someRecord = {nested: Some({maybeVariant: Three(false, So)})} +// ^co2 + +// let myFunc: someRecord = {nested: Some({maybeVariant: One}), variant: } +// ^co2 + +// let myFunc: someRecord = {nested: Some({maybeVariant: One, })} +// ^co2 + +// let myFunc: someRecord = {nested: Some({maybeVariant: One}), } +// ^co2 + +// This should reset the context, meaning it should just complete for the identifier +// let myFunc: someRecord = {nested: {maybeVariant: {let x = true; if x {}}}, } +// ^co2 + +// This is the last expression - NOTE: This should work but it's doing a follow +// let myFunc: someRecord = {nested: {maybeVariant: {let x = true; if x {}}}, } +// ^co2 + +// Complete as the last expression (looking for the record field type) - NOTE: This should work but it's doing a follow +// let myFunc: someRecord = {nested: {maybeVariant: {doStuff(); let x = true; if x {So}}}, } +// ^co2 + +// Complete on the identifier, no context +// let myFunc: someRecord = {nested: {maybeVariant: {doStuff(); let x = true; if x {v}}}, } +// ^co2 + +type fn = (~name: string=?, string) => bool + +// let getBool = (name): bool => +// ^co2 + +// let someFun: fn = (str, ~name) => {} +// ^co2 + +// let someFun: fn = (str, ~name) => {let whatever = true; if whatever {}} +// ^co2 + +// A let binding with an annotation. Reset to annotated constraint. +// let someFun: fn = (str, ~name) => {let whatever: bool = t} +// ^co2 + +// A let binding without annotation. Point to inferred type if it has compiled. +// let someFun: fn = (str, ~name) => {let whatever = t} +// ^co2 + +// == Let binding patterns == +// let someVar: bool = +// ^co2 + +// let {someField: s } = someRecordVar +// ^co2 + +// == Tuple patterns == +// let (true, ) = someRecordVar +// ^co2 + +// let (true, true, , false) = someRecordVar +// ^co2 + +// == Arrays == +// let [ ] = someArr +// ^co2 + +// let [(true, [false, ])] = someArr +// ^co2 + +// let [(true, [false, f])] = someArr +// ^co2 + +// == Apply == +// let x = if true && f {None} +// ^co2 + +// let x = someFunc(() => {let x = true; f}) +// ^co2 + +// let x = someFunc(~labelledArg=f) +// ^co2 + +// let x = someFunc(~labelledArg=) +// ^co2 + +// == Pipes == +// let x = foo->id +// ^co2 + +// let x = foo-> +// ^co2 + +// let x = foo->M +// ^co2 + +// == Function arguments == + +let someFun = (~firstLabel, ~secondLabel=?, r: someRecord) => { + firstLabel ++ secondLabel->Belt.Option.getWithDefault("") ++ r.someString +} + +// let ff = someFun(~secondLabel, ~f) +// ^co2 + +// let ff = someFun(~secondLabel, ~f) +// ^co2 + +// == JSX == +// let jsx = 2:13] , prefix: "m" +Package opens Pervasives.JsxModules.place holder +Resolved opens 1 pervasives +CTypeAtLoc: found no type at loc +[{ + "label": "myVar", + "kind": 12, + "tags": [], + "detail": "bool", + "documentation": null + }] + +Complete2 src/CompletionNew.res 7:30 +Scope: 9 items +Completable: Cexpression: ctxPath: Type(prefix=someVariant) , prefix: "O" +Package opens Pervasives.JsxModules.place holder +Resolved opens 1 pervasives +Path someVariant +[{ + "label": "One", + "kind": 4, + "tags": [], + "detail": "One\n\ntype someVariant =\n | One\n | Two\n | Three(bool, option)", + "documentation": null, + "sortText": "A One", + "insertText": "One", + "insertTextFormat": 2 + }, { + "label": "Obj", + "kind": 9, + "tags": [], + "detail": "file module", + "documentation": null + }, { + "label": "Objects", + "kind": 9, + "tags": [], + "detail": "file module", + "documentation": null + }] + +Complete2 src/CompletionNew.res 10:36 +Scope: 9 items +Completable: Cexpression: ctxPath: (Type(prefix=someVariant))->variantPayload(Three<$0>) , prefix: "t" +Package opens Pervasives.JsxModules.place holder +Resolved opens 1 pervasives +Path someVariant +CVariantPayload: found payload type: bool +[{ + "label": "true", + "kind": 4, + "tags": [], + "detail": "bool", + "documentation": null + }] + +Complete2 src/CompletionNew.res 13:43 +Scope: 9 items +Completable: Cexpression: ctxPath: (Type(prefix=someVariant))->variantPayload(Three<$1>) , prefix: "So" +Package opens Pervasives.JsxModules.place holder +Resolved opens 1 pervasives +Path someVariant +CVariantPayload: found payload type: option +[{ + "label": "Some(_)", + "kind": 12, + "tags": [], + "detail": "someVariant", + "documentation": null, + "sortText": "A Some(_)", + "insertText": "Some(${1:_})", + "insertTextFormat": 2 + }, { + "label": "Sort", + "kind": 9, + "tags": [], + "detail": "file module", + "documentation": null + }] + +Complete2 src/CompletionNew.res 16:47 +Scope: 9 items +Completable: Cexpression: ctxPath: ((Type(prefix=someVariant))->variantPayload(Three<$1>))->variantPayload(Some<$0>) , prefix: "O" +Package opens Pervasives.JsxModules.place holder +Resolved opens 1 pervasives +Path someVariant +CVariantPayload: found payload type: option +[{ + "label": "One", + "kind": 4, + "tags": [], + "detail": "One\n\ntype someVariant =\n | One\n | Two\n | Three(bool, option)", + "documentation": null, + "sortText": "A One", + "insertText": "One", + "insertTextFormat": 2 + }, { + "label": "Obj", + "kind": 9, + "tags": [], + "detail": "file module", + "documentation": null + }, { + "label": "Objects", + "kind": 9, + "tags": [], + "detail": "file module", + "documentation": null + }] + +Complete2 src/CompletionNew.res 27:29 +Scope: 105 items +Completable: Cexpression: ctxPath: Type(prefix=someRecord)->recordField("", []) +Package opens Pervasives.JsxModules.place holder +Resolved opens 1 pervasives +Path someRecord +CRecordField: found record: type someRecord = {nested: option, variant: someVariant, someString: string} +[{ + "label": "nested", + "kind": 5, + "tags": [], + "detail": "nested: option\n\ntype someRecord = {nested: option, variant: someVariant, someString: string}", + "documentation": null + }, { + "label": "variant", + "kind": 5, + "tags": [], + "detail": "variant: someVariant\n\ntype someRecord = {nested: option, variant: someVariant, someString: string}", + "documentation": null + }, { + "label": "someString", + "kind": 5, + "tags": [], + "detail": "someString: string\n\ntype someRecord = {nested: option, variant: someVariant, someString: string}", + "documentation": null + }] + +Complete2 src/CompletionNew.res 30:30 +Scope: 105 items +Completable: Cexpression: ctxPath: Type(prefix=someRecord)->recordField("n", []) , prefix: "n" +Package opens Pervasives.JsxModules.place holder +Resolved opens 1 pervasives +Path someRecord +CRecordField: found record: type someRecord = {nested: option, variant: someVariant, someString: string} +[{ + "label": "nested", + "kind": 5, + "tags": [], + "detail": "nested: option\n\ntype someRecord = {nested: option, variant: someVariant, someString: string}", + "documentation": null + }] + +Complete2 src/CompletionNew.res 33:39 +Scope: 105 items +Completable: Cexpression: ctxPath: Type(prefix=someRecord) , prefix: "O" +Package opens Pervasives.JsxModules.place holder +Resolved opens 1 pervasives +Path someRecord +[{ + "label": "Obj", + "kind": 9, + "tags": [], + "detail": "file module", + "documentation": null + }, { + "label": "Objects", + "kind": 9, + "tags": [], + "detail": "file module", + "documentation": null + }] + +Complete2 src/CompletionNew.res 36:72 +Scope: 105 items +Completable: Cexpression: ctxPath: ((Type(prefix=someRecord))->variantPayload(Some<$0>))->variantPayload(Three<$1>) , prefix: "So" +Package opens Pervasives.JsxModules.place holder +Resolved opens 1 pervasives +Path someRecord +CVariantPayload(constructorName: Some, itemNum: 0): some other type than a variant found at variant ctx path: type someRecord = {nested: option, variant: someVariant, someString: string} +CVariantPayload: did not find type at variant ctx path +[{ + "label": "Sort", + "kind": 9, + "tags": [], + "detail": "file module", + "documentation": null + }] + +Complete2 src/CompletionNew.res 39:72 +Scope: 105 items +Completable: Cexpression: ctxPath: (Type(prefix=someRecord))->followRecordField{variant} +Package opens Pervasives.JsxModules.place holder +Resolved opens 1 pervasives +Path someRecord +CRecordFieldFollow: type of field ("variant") to follow: someVariant +[{ + "label": "One", + "kind": 4, + "tags": [], + "detail": "One\n\ntype someVariant =\n | One\n | Two\n | Three(bool, option)", + "documentation": null, + "insertText": "One", + "insertTextFormat": 2 + }, { + "label": "Two", + "kind": 4, + "tags": [], + "detail": "Two\n\ntype someVariant =\n | One\n | Two\n | Three(bool, option)", + "documentation": null, + "insertText": "Two", + "insertTextFormat": 2 + }, { + "label": "Three(_, _)", + "kind": 4, + "tags": [], + "detail": "Three(bool, option)\n\ntype someVariant =\n | One\n | Two\n | Three(bool, option)", + "documentation": null, + "insertText": "Three(${1:_}, ${2:_})", + "insertTextFormat": 2 + }] + +Complete2 src/CompletionNew.res 42:61 +Scope: 105 items +Completable: Cexpression: ctxPath: (Type(prefix=someRecord))->variantPayload(Some<$0>)->recordField("", [maybeVariant]) +Package opens Pervasives.JsxModules.place holder +Resolved opens 1 pervasives +Path someRecord +CVariantPayload(constructorName: Some, itemNum: 0): some other type than a variant found at variant ctx path: type someRecord = {nested: option, variant: someVariant, someString: string} +CRecordField: found no type at record ctx path +[] + +Complete2 src/CompletionNew.res 45:63 +Scope: 105 items +Completable: Cexpression: ctxPath: Type(prefix=someRecord)->recordField("", [nested]) +Package opens Pervasives.JsxModules.place holder +Resolved opens 1 pervasives +Path someRecord +CRecordField: found record: type someRecord = {nested: option, variant: someVariant, someString: string} +[{ + "label": "variant", + "kind": 5, + "tags": [], + "detail": "variant: someVariant\n\ntype someRecord = {nested: option, variant: someVariant, someString: string}", + "documentation": null + }, { + "label": "someString", + "kind": 5, + "tags": [], + "detail": "someString: string\n\ntype someRecord = {nested: option, variant: someVariant, someString: string}", + "documentation": null + }] + +Complete2 src/CompletionNew.res 49:71 +Scope: 106 items +Completable: Cexpression: ctxPath: CUnknown , prefix: "x" +Package opens Pervasives.JsxModules.place holder +Resolved opens 1 pervasives +[{ + "label": "x", + "kind": 12, + "tags": [], + "detail": "\\\"Type Not Known\"", + "documentation": null + }] + +Complete2 src/CompletionNew.res 53:73 +Scope: 106 items +Completable: Cexpression: ctxPath: Type(prefix=someRecord) +Package opens Pervasives.JsxModules.place holder +Resolved opens 1 pervasives +Path someRecord +[{ + "label": "{}", + "kind": 12, + "tags": [], + "detail": "type someRecord = {nested: option, variant: someVariant, someString: string}", + "documentation": null, + "sortText": "A", + "insertText": "{$0}", + "insertTextFormat": 2 + }] + +Complete2 src/CompletionNew.res 57:86 +Scope: 106 items +Completable: Cexpression: ctxPath: Type(prefix=someRecord) , prefix: "So" +Package opens Pervasives.JsxModules.place holder +Resolved opens 1 pervasives +Path someRecord +[{ + "label": "Sort", + "kind": 9, + "tags": [], + "detail": "file module", + "documentation": null + }] + +Complete2 src/CompletionNew.res 61:58 +Scope: 105 items +Completable: Cexpression: ctxPath: CUnknown , prefix: "doStuff" +Package opens Pervasives.JsxModules.place holder +Resolved opens 1 pervasives +[] + +Complete2 src/CompletionNew.res 66:32 +Scope: 107 items +Completable: Cexpression: ctxPath: Value(prefix=) +Package opens Pervasives.JsxModules.place holder +Resolved opens 1 pervasives +Path +[] + +Complete2 src/CompletionNew.res 69:38 +Scope: 108 items +Completable: Cexpression: ctxPath: Type(prefix=fn)->recordField("", []) +Package opens Pervasives.JsxModules.place holder +Resolved opens 1 pervasives +Path fn +CRecordField: found something that's not a record at the record ctx path: (~name: string=?, string) => bool +[] + +Complete2 src/CompletionNew.res 72:72 +Scope: 109 items +Completable: Cexpression: ctxPath: Type(prefix=fn) +Package opens Pervasives.JsxModules.place holder +Resolved opens 1 pervasives +Path fn +[{ + "label": "(~name=?, v1) => {}", + "kind": 12, + "tags": [], + "detail": "(~name: string=?, string) => bool", + "documentation": null, + "sortText": "A", + "insertText": "(~name=?, ${1:v1}) => {$0}", + "insertTextFormat": 2 + }] + +Complete2 src/CompletionNew.res 76:60 +Scope: 108 items +Completable: Cexpression: ctxPath: bool , prefix: "t" +Package opens Pervasives.JsxModules.place holder +Resolved opens 1 pervasives +CBool: returning bool +[{ + "label": "true", + "kind": 4, + "tags": [], + "detail": "bool", + "documentation": null + }] + +Complete2 src/CompletionNew.res 80:54 +Scope: 108 items +Completable: Cexpression: ctxPath: CTypeAtLoc: [80:42->80:50] , prefix: "t" +Package opens Pervasives.JsxModules.place holder +Resolved opens 1 pervasives +CTypeAtLoc: found no type at loc +[] + +Complete2 src/CompletionNew.res 84:22 +Scope: 106 items +Completable: CtxPath: Value(prefix=) +Package opens Pervasives.JsxModules.place holder +Resolved opens 1 pervasives +Path +[] + +Complete2 src/CompletionNew.res 87:20 +Scope: 106 items +Completable: Cpattern: ctxPath: CUnknown->recordField("someField", [someField]), , prefix: "s" +Package opens Pervasives.JsxModules.place holder +Resolved opens 1 pervasives +CRecordField: found no type at record ctx path +[] + +Complete2 src/CompletionNew.res 91:13 +Scope: 106 items +Completable: Cpattern: ctxPath: CUnknown->tupleItem($1), +Package opens Pervasives.JsxModules.place holder +Resolved opens 1 pervasives +CTupleItem: no type found at tuple ctx path +[] + +Complete2 src/CompletionNew.res 94:20 +Scope: 106 items +Completable: Cpattern: ctxPath: CUnknown->tupleItem($2), +Package opens Pervasives.JsxModules.place holder +Resolved opens 1 pervasives +CTupleItem: no type found at tuple ctx path +[] + +Complete2 src/CompletionNew.res 98:9 +Scope: 106 items +Completable: Cpattern: ctxPath: array, +Package opens Pervasives.JsxModules.place holder +Resolved opens 1 pervasives +CArray: array with no payload +[] + +Complete2 src/CompletionNew.res 101:22 +Scope: 106 items +Completable: Cpattern: ctxPath: array, +Package opens Pervasives.JsxModules.place holder +Resolved opens 1 pervasives +CArray: array with no payload +[] + +Complete2 src/CompletionNew.res 104:24 +Scope: 106 items +Completable: Cpattern: ctxPath: array, , prefix: "f" +Package opens Pervasives.JsxModules.place holder +Resolved opens 1 pervasives +CArray: array with no payload +[] + +Complete2 src/CompletionNew.res 108:23 +Scope: 106 items +Completable: Cexpression: ctxPath: CUnknown , prefix: "f" +Package opens Pervasives.JsxModules.place holder +Resolved opens 1 pervasives +[] + +Complete2 src/CompletionNew.res 111:42 +Scope: 107 items +Completable: Cexpression: ctxPath: CUnknown , prefix: "f" +Package opens Pervasives.JsxModules.place holder +Resolved opens 1 pervasives +[] + +Complete2 src/CompletionNew.res 114:34 +Scope: 106 items +Completable: Cexpression: ctxPath: CUnknown , prefix: "f" +Package opens Pervasives.JsxModules.place holder +Resolved opens 1 pervasives +[] + +Complete2 src/CompletionNew.res 117:33 +Scope: 106 items +Completable: Cexpression: ctxPath: CUnknown +Package opens Pervasives.JsxModules.place holder +Resolved opens 1 pervasives +[] + +Complete2 src/CompletionNew.res 121:17 +Scope: 106 items +Completable: CtxPath: pipe(Value(prefix=foo))->id +Package opens Pervasives.JsxModules.place holder +Resolved opens 1 pervasives +Path foo +CPipe: could not look up type at pipe fn ctx path +[] + +Complete2 src/CompletionNew.res 124:16 +Scope: 106 items +Completable: CtxPath: pipe(Value(prefix=foo))-> +Package opens Pervasives.JsxModules.place holder +Resolved opens 1 pervasives +Path foo +CPipe: could not look up type at pipe fn ctx path +[] + +Complete2 src/CompletionNew.res 127:17 +Scope: 106 items +Completable: Cexpression: ctxPath: CUnknown , prefix: "M" +Package opens Pervasives.JsxModules.place holder +Resolved opens 1 pervasives +[{ + "label": "Map", + "kind": 9, + "tags": [], + "detail": "file module", + "documentation": null + }, { + "label": "MapLabels", + "kind": 9, + "tags": [], + "detail": "file module", + "documentation": null + }, { + "label": "MoreLabels", + "kind": 9, + "tags": [], + "detail": "file module", + "documentation": null + }] + +Complete2 src/CompletionNew.res 136:36 +Scope: 107 items +Completable: CnamedArg(Value(prefix=someFun), f, [secondLabel, f]) +Package opens Pervasives.JsxModules.place holder +Resolved opens 1 pervasives +Path someFun +Found type for function ( + ~firstLabel: string, + ~secondLabel: string=?, + someRecord, +) => string +[{ + "label": "firstLabel", + "kind": 4, + "tags": [], + "detail": "string", + "documentation": null + }] + +Complete2 src/CompletionNew.res 139:37 +Scope: 107 items +Completable: Cexpression: ctxPath: CUnknown +Package opens Pervasives.JsxModules.place holder +Resolved opens 1 pervasives +[] + +Complete2 src/CompletionNew.res 143:21 +Scope: 107 items +Completable: CtxPath: Module(prefix=SomeCom) +Package opens Pervasives.JsxModules.place holder +Resolved opens 1 pervasives +Path SomeCom +[] + +Complete2 src/CompletionNew.res 146:26 +Scope: 107 items +Completable: CtxPath: Module(prefix=SomeModule.S) +Package opens Pervasives.JsxModules.place holder +Resolved opens 1 pervasives +Path SomeModule.S +[] + +Complete2 src/CompletionNew.res 149:24 +No completions +[] + +Complete2 src/CompletionNew.res 152:25 +Scope: 107 items +Completable: Cjsx(Component, a, [a]) +Package opens Pervasives.JsxModules.place holder +Resolved opens 1 pervasives +Path Component.make +[] + +Complete2 src/CompletionNew.res 155:30 +Scope: 107 items +Completable: Cexpression: ctxPath: CTypeAtLoc: [155:7->155:10] +Package opens Pervasives.JsxModules.place holder +Resolved opens 1 pervasives +CTypeAtLoc: found no type at loc +[] + +Complete2 src/CompletionNew.res 158:40 +Scope: 107 items +Completable: Cexpression: ctxPath: CTypeAtLoc: [158:7->158:10] +Package opens Pervasives.JsxModules.place holder +Resolved opens 1 pervasives +CTypeAtLoc: found no type at loc +[] + +Complete2 src/CompletionNew.res 161:35 +Scope: 107 items +Completable: Cexpression: ctxPath: CTypeAtLoc: [161:7->161:10] , prefix: "Stuff" +Package opens Pervasives.JsxModules.place holder +Resolved opens 1 pervasives +CTypeAtLoc: found no type at loc +[] +