Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Complete function argument values #665

Merged
merged 29 commits into from
Dec 29, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
a1c0595
add base test for what we're looking to complete
zth Dec 22, 2022
f3dbd68
add test cases and identify arguments for completion
zth Dec 22, 2022
1bbb35f
refactor in prep for reuse
zth Dec 22, 2022
3d1a03e
add utils for extracting relevant completion information from type expr
zth Dec 22, 2022
06e25c7
complete bools as arguments
zth Dec 22, 2022
284ecb4
remove things from type extraction that will be added in separate PRs
zth Dec 23, 2022
ecc1693
complete regular variants
zth Dec 23, 2022
8fb62d3
add failing tests for optionals
zth Dec 23, 2022
0546ce4
basic completion for opts
zth Dec 23, 2022
ac348bb
expand options where it makes sense
zth Dec 23, 2022
379dd1a
remove unused test case
zth Dec 23, 2022
7b66a3d
make sure payloads (with any as placeholder) are printed for each con…
zth Dec 23, 2022
840cdb5
add failing test demonstrating issue with parser
zth Dec 23, 2022
8ec981a
include local values and modules in type based completions when there…
zth Dec 23, 2022
8d261be
fix full variant completion item text
zth Dec 25, 2022
6d10c56
add more cases to tests
zth Dec 25, 2022
17832b5
add = as trigger character for completion
zth Dec 25, 2022
f0e894e
cleanup
zth Dec 25, 2022
41191a8
cleanup
zth Dec 25, 2022
8755a92
comment + clarify prop name
zth Dec 25, 2022
aebffa4
polish test output a bit
zth Dec 25, 2022
e5a0950
only pick up regular Lident
zth Dec 25, 2022
8d81bc8
use env where completion started to populate values and module comple…
zth Dec 27, 2022
9856c30
common filter
zth Dec 27, 2022
2216856
fix triggering value completion vs named arg ambiguity
zth Dec 27, 2022
0774fc0
handle piped fn calls properly
zth Dec 27, 2022
619d7ec
move broken parser cases to its own file
zth Dec 28, 2022
5995f45
only expand options on optional arguments, not all labelled arguments
zth Dec 28, 2022
0f62611
add changelog
zth Dec 29, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@

## v1.10.0

#### :rocket: New Feature

- Add autocomplete for function argument values (booleans, variants and options. More values coming), both labelled and unlabelled. https://github.com/rescript-lang/rescript-vscode/pull/665

#### :nail_care: Polish

- Remove spacing between type definition in clients that do not support markdown links. https://github.com/rescript-lang/rescript-vscode/pull/619
Expand Down
179 changes: 156 additions & 23 deletions analysis/src/CompletionBackEnd.ml
Original file line number Diff line number Diff line change
Expand Up @@ -1455,6 +1455,123 @@ let getOpens ~debug ~rawOpens ~package ~env =
(* Last open takes priority *)
List.rev resolvedOpens

let getArgs ~env (t : Types.type_expr) ~full =
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This one is mostly copied from a pre-existing fn, but with identifying what type of argument is found (labelled or unlabelled).

let rec getArgsLoop ~env (t : Types.type_expr) ~full ~currentArgumentPosition
=
match t.desc with
| Tlink t1 | Tsubst t1 | Tpoly (t1, []) ->
getArgsLoop ~full ~env ~currentArgumentPosition t1
| Tarrow (Labelled l, tArg, tRet, _) ->
(SharedTypes.Completable.Labelled l, tArg)
:: getArgsLoop ~full ~env ~currentArgumentPosition tRet
| Tarrow (Optional l, tArg, tRet, _) ->
(Optional l, tArg) :: getArgsLoop ~full ~env ~currentArgumentPosition tRet
| Tarrow (Nolabel, tArg, tRet, _) ->
(Unlabelled {argumentPosition = currentArgumentPosition}, tArg)
:: getArgsLoop ~full ~env
~currentArgumentPosition:(currentArgumentPosition + 1)
tRet
| Tconstr (path, typeArgs, _) -> (
match References.digConstructor ~env ~package:full.package path with
| Some
( env,
{
item = {decl = {type_manifest = Some t1; type_params = typeParams}};
} ) ->
let t1 = t1 |> instantiateType ~typeParams ~typeArgs in
getArgsLoop ~full ~env ~currentArgumentPosition t1
| _ -> [])
| _ -> []
in
t |> getArgsLoop ~env ~full ~currentArgumentPosition:0

(** Pulls out a type we can complete from a type expr. *)
let rec extractType ~env ~package (t : Types.type_expr) =
match t.desc with
| Tlink t1 | Tsubst t1 | Tpoly (t1, []) -> extractType ~env ~package t1
| Tconstr (Path.Pident {name = "option"}, [payloadTypeExpr], _) ->
Some (Completable.Toption (env, payloadTypeExpr))
| Tconstr (Path.Pident {name = "bool"}, [], _) -> Some (Tbool env)
| Tconstr (path, _, _) -> (
match References.digConstructor ~env ~package path with
| Some (env, {item = {decl = {type_manifest = Some t1}}}) ->
extractType ~env ~package t1
| Some (env, {name; item = {decl; kind = Type.Variant constructors}}) ->
Some
(Tvariant
{env; constructors; variantName = name.txt; variantDecl = decl})
| _ -> None)
| Ttuple expressions -> Some (Tuple (env, expressions))
| _ -> None

let filterItems items ~prefix =
if prefix = "" then items
else
items
|> List.filter (fun (item : Completion.t) ->
Utils.startsWith item.name prefix)

let completeTypedValue ~env ~envWhereCompletionStarted ~full ~prefix
~expandOption =
let namesUsed = Hashtbl.create 10 in
let rec completeTypedValueInner t ~env ~full ~prefix ~expandOption =
let items =
match t |> extractType ~env ~package:full.package with
| Some (Toption (env, typ)) when expandOption ->
typ |> completeTypedValueInner ~env ~full ~prefix ~expandOption:false
| Some (Tbool env) ->
[
Completion.create ~name:"true"
~kind:(Label (t |> Shared.typeToString))
~env;
Completion.create ~name:"false"
~kind:(Label (t |> Shared.typeToString))
~env;
]
|> filterItems ~prefix
| Some (Tvariant {env; constructors; variantDecl; variantName}) ->
constructors
|> List.map (fun (constructor : Constructor.t) ->
Completion.create
~name:
(constructor.cname.txt
^
if constructor.args |> List.length > 0 then
"("
^ (constructor.args
|> List.map (fun _ -> "_")
|> String.concat ", ")
^ ")"
else "")
~kind:
(Constructor
( constructor,
variantDecl |> Shared.declToString variantName ))
~env)
|> filterItems ~prefix
| Some (Toption (env, t)) ->
[
Completion.create ~name:"None"
~kind:(Label (t |> Shared.typeToString))
~env;
Completion.create ~name:"Some(_)"
~kind:(Label (t |> Shared.typeToString))
~env;
]
|> filterItems ~prefix
| _ -> []
in
(* Include all values and modules in completion if there's a prefix, not otherwise *)
if prefix = "" then items
else
items
@ completionForExportedValues ~env:envWhereCompletionStarted ~prefix
~exact:false ~namesUsed
@ completionForExportedModules ~env:envWhereCompletionStarted ~prefix
~exact:false ~namesUsed
in
completeTypedValueInner ~env ~full ~prefix ~expandOption

let processCompletable ~debug ~full ~scope ~env ~pos ~forHover
(completable : Completable.t) =
let package = full.package in
Expand Down Expand Up @@ -1817,6 +1934,38 @@ Note: The `@react.component` decorator requires the react-jsx config to be set i
in
(dec2, doc))
|> List.map mkDecorator
| Cargument {functionContextPath; argumentLabel; prefix} -> (
let envWhereCompletionStarted = env in
let labels =
match
functionContextPath
|> getCompletionsForContextPath ~full ~opens ~rawOpens ~allFiles ~pos
~env ~exact:true ~scope
|> completionsGetTypeEnv
with
| Some (typ, _env) -> typ |> getArgs ~full ~env
| None -> []
in
let targetLabel =
labels
|> List.find_opt (fun (label, _) ->
match argumentLabel with
| Unlabelled _ -> label = argumentLabel
| Labelled name | Optional name -> (
match label with
| (Labelled n | Optional n) when name = n -> true
| _ -> false))
in
match targetLabel with
| None -> []
| Some (Optional _, typ) ->
typ
|> completeTypedValue ~env ~envWhereCompletionStarted ~full ~prefix
~expandOption:true
| Some ((Unlabelled _ | Labelled _), typ) ->
typ
|> completeTypedValue ~env ~envWhereCompletionStarted ~full ~prefix
~expandOption:false)
| CnamedArg (cp, prefix, identsSeen) ->
let labels =
match
Expand All @@ -1829,29 +1978,13 @@ Note: The `@react.component` decorator requires the react-jsx config to be set i
if debug then
Printf.printf "Found type for function %s\n"
(typ |> Shared.typeToString);
let rec getLabels ~env (t : Types.type_expr) =
match t.desc with
| Tlink t1 | Tsubst t1 | Tpoly (t1, []) -> getLabels ~env t1
| Tarrow ((Labelled l | Optional l), tArg, tRet, _) ->
(l, tArg) :: getLabels ~env tRet
| Tarrow (Nolabel, _, tRet, _) -> getLabels ~env tRet
| Tconstr (path, typeArgs, _) -> (
match References.digConstructor ~env ~package path with
| Some
( env,
{
item =
{
decl =
{type_manifest = Some t1; type_params = typeParams};
};
} ) ->
let t1 = t1 |> instantiateType ~typeParams ~typeArgs in
getLabels ~env t1
| _ -> [])
| _ -> []
in
typ |> getLabels ~env

typ |> 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) =
Expand Down
132 changes: 118 additions & 14 deletions analysis/src/CompletionFrontEnd.ml
Original file line number Diff line number Diff line change
Expand Up @@ -106,8 +106,24 @@ let extractJsxProps ~(compName : Longident.t Location.loc) ~args =
in
args |> processProps ~acc:[]

let findNamedArgCompletable ~(args : arg list) ~endPos ~posBeforeCursor
~(contextPath : Completable.contextPath) ~posAfterFunExpr =
let extractCompletableArgValueInfo exp =
match exp.Parsetree.pexp_desc with
| Pexp_ident {txt = Lident txt} -> Some txt
| Pexp_construct ({txt = Lident "()"}, _) -> Some ""
| Pexp_construct ({txt = Lident txt}, _) -> Some txt
| _ -> None

let isExprHole exp =
match exp.Parsetree.pexp_desc with
| Pexp_extension ({txt = "rescript.exprhole"}, _) -> true
| _ -> false

let findArgCompletables ~(args : arg list) ~endPos ~posBeforeCursor
~(contextPath : Completable.contextPath) ~posAfterFunExpr ~charBeforeCursor
~isPipedExpr =
let fnHasCursor =
posAfterFunExpr <= posBeforeCursor && posBeforeCursor < endPos
in
let allNames =
List.fold_right
(fun arg allLabels ->
Expand All @@ -116,24 +132,90 @@ let findNamedArgCompletable ~(args : arg list) ~endPos ~posBeforeCursor
| {label = None} -> allLabels)
args []
in
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 <= posBeforeCursor
&& posBeforeCursor < labelled.posEnd
then Some (Completable.CnamedArg (contextPath, labelled.name, allNames))
else if exp.pexp_loc |> Loc.hasPos ~pos:posBeforeCursor then None
else if exp.pexp_loc |> Loc.hasPos ~pos:posBeforeCursor then
(* Completing in the assignment of labelled argument *)
match extractCompletableArgValueInfo exp with
| None -> None
| Some prefix ->
Some
(Cargument
{
functionContextPath = contextPath;
argumentLabel = Labelled labelled.name;
prefix;
})
else if isExprHole exp then
Some
(Cargument
{
functionContextPath = contextPath;
argumentLabel = Labelled labelled.name;
prefix = "";
})
else loop rest
| {label = None; exp} :: rest ->
if exp.pexp_loc |> Loc.hasPos ~pos:posBeforeCursor then None
else loop rest
if Res_parsetree_viewer.isTemplateLiteral exp then None
else if exp.pexp_loc |> Loc.hasPos ~pos:posBeforeCursor then
(* Completing in an unlabelled argument *)
match extractCompletableArgValueInfo exp with
| None -> None
| Some prefix ->
Some
(Cargument
{
functionContextPath = contextPath;
argumentLabel =
Unlabelled {argumentPosition = !unlabelledCount};
prefix;
})
else if isExprHole exp then
Some
(Cargument
{
functionContextPath = contextPath;
argumentLabel = Unlabelled {argumentPosition = !unlabelledCount};
prefix = "";
})
else (
unlabelledCount := !unlabelledCount + 1;
loop rest)
| [] ->
if posAfterFunExpr <= posBeforeCursor && posBeforeCursor < endPos then
Some (CnamedArg (contextPath, "", allNames))
if fnHasCursor then
match charBeforeCursor with
| Some '~' -> Some (Completable.CnamedArg (contextPath, "", allNames))
| _ ->
Some
(Cargument
{
functionContextPath = contextPath;
argumentLabel =
Unlabelled {argumentPosition = !unlabelledCount};
prefix = "";
})
else None
in
loop args
match args with
(* Special handling for empty fn calls, e.g. `let _ = someFn(<com>)` *)
| [
{label = None; exp = {pexp_desc = Pexp_construct ({txt = Lident "()"}, _)}};
]
when fnHasCursor ->
Some
(Completable.Cargument
{
functionContextPath = contextPath;
argumentLabel = Unlabelled {argumentPosition = 0};
prefix = "";
})
| _ -> loop args

let rec exprToContextPath (e : Parsetree.expression) =
match e.pexp_desc with
Expand Down Expand Up @@ -553,15 +635,36 @@ let completionWithParser1 ~currentFile ~debug ~offset ~path ~posCursor ~text =
when Loc.end_ opLoc = posCursor ->
(* Case foo-> *)
setPipeResult ~lhs ~id:"" |> ignore
| Pexp_apply ({pexp_desc = Pexp_ident {txt = Lident "|."}}, [_; _]) ->
()
| Pexp_apply (funExpr, args)
| Pexp_apply
( {pexp_desc = Pexp_ident {txt = Lident "|."}},
[_; (_, {pexp_desc = Pexp_apply (funExpr, args)})] )
when (* Normally named arg completion fires when the cursor is right after the expression.
E.g in foo(~<---there
But it should not fire in foo(~a)<---there *)
not
(Loc.end_ expr.pexp_loc = posCursor
&& charBeforeCursor = Some ')') ->
(* Complete fn argument values and named args when the fn call is piped. E.g. someVar->someFn(<com>). *)
let args = extractExpApplyArgs ~args in
let argCompletable =
match exprToContextPath funExpr with
| Some contextPath ->
findArgCompletables ~contextPath ~args
~endPos:(Loc.end_ expr.pexp_loc) ~posBeforeCursor
~posAfterFunExpr:(Loc.end_ funExpr.pexp_loc)
~charBeforeCursor ~isPipedExpr:true
| None -> None
in

setResultOpt argCompletable
| Pexp_apply ({pexp_desc = Pexp_ident {txt = Lident "|."}}, [_; _]) ->
(* Ignore any other pipe. *)
()
| Pexp_apply (funExpr, args)
when not
(Loc.end_ expr.pexp_loc = posCursor
&& charBeforeCursor = Some ')') ->
(* Complete fn argument values and named args when the fn call is _not_ piped. E.g. someFn(<com>). *)
let args = extractExpApplyArgs ~args in
if debug then
Printf.printf "Pexp_apply ...%s (%s)\n"
Expand All @@ -578,16 +681,17 @@ let completionWithParser1 ~currentFile ~debug ~offset ~path ~posCursor ~text =
(Loc.toString exp.pexp_loc))
|> String.concat ", ");

let namedArgCompletable =
let argCompletable =
match exprToContextPath funExpr with
| Some contextPath ->
findNamedArgCompletable ~contextPath ~args
findArgCompletables ~contextPath ~args
~endPos:(Loc.end_ expr.pexp_loc) ~posBeforeCursor
~posAfterFunExpr:(Loc.end_ funExpr.pexp_loc)
~charBeforeCursor ~isPipedExpr:false
| None -> None
in

setResultOpt namedArgCompletable
setResultOpt argCompletable
| Pexp_send (lhs, {txt; loc}) -> (
(* e["txt"]
If the string for txt is not closed, it could go over several lines.
Expand Down
Loading