Skip to content

Commit 971e708

Browse files
committedAug 27, 2023
pipe completion
1 parent 3bc47b4 commit 971e708

File tree

4 files changed

+233
-32
lines changed

4 files changed

+233
-32
lines changed
 

‎analysis/src/CompletionFrontEndNew.ml

+182-32
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,11 @@ end
6060

6161
type completionCategory = Type | Value | Module | Field
6262

63+
type argumentLabel =
64+
| Unlabelled of {argumentPosition: int}
65+
| Labelled of string
66+
| Optional of string
67+
6368
type ctxPath =
6469
| CUnknown (** Something that cannot be resolved right now *)
6570
| CId of string list * completionCategory
@@ -87,6 +92,15 @@ type ctxPath =
8792
and the string is the name of the property we're accessing. *)
8893
| CApply of ctxPath * Asttypes.arg_label list
8994
(** Function application. `someFunction(someVar, ~otherLabel="hello")`. The ctxPath points to the function. *)
95+
| CFunctionArgument of {
96+
functionContextPath: ctxPath;
97+
argumentLabel: argumentLabel;
98+
} (** A function argument, either labelled or unlabelled.*)
99+
| CPipe of {
100+
ctxPath: ctxPath; (** Context path to the function being called. *)
101+
id: string;
102+
lhsLoc: Location.t; (** Location of the left hand side. *)
103+
} (** Piped call. `foo->someFn`. *)
90104

91105
let rec ctxPathToString (ctxPath : ctxPath) =
92106
match ctxPath with
@@ -130,6 +144,16 @@ let rec ctxPathToString (ctxPath : ctxPath) =
130144
(match ctxPath with
131145
| None -> ""
132146
| Some ctxPath -> "<" ^ ctxPathToString ctxPath ^ ">")
147+
| CFunctionArgument {functionContextPath; argumentLabel} ->
148+
"CFunctionArgument "
149+
^ (functionContextPath |> ctxPathToString)
150+
^ "("
151+
^ (match argumentLabel with
152+
| Unlabelled {argumentPosition} -> "$" ^ string_of_int argumentPosition
153+
| Labelled name -> "~" ^ name
154+
| Optional name -> "~" ^ name ^ "=?")
155+
^ ")"
156+
| CPipe {ctxPath; id} -> "(" ^ ctxPathToString ctxPath ^ ")->" ^ id
133157

134158
type currentlyExpecting =
135159
| Unit (** Unit, (). Is what we reset to. *)
@@ -437,6 +461,37 @@ let checkIfPatternHoleEmptyCursor ~(completionContext : CompletionContext.t)
437461
~pos:completionContext.positionContext.beforeCursor
438462
= EmptyLoc
439463

464+
let completePipeChain (exp : Parsetree.expression) =
465+
(* Complete the end of pipe chains by reconstructing the pipe chain as a single pipe,
466+
so it can be completed.
467+
Example:
468+
someArray->Js.Array2.filter(v => v > 10)->Js.Array2.map(v => v + 2)->
469+
will complete as:
470+
Js.Array2.map(someArray->Js.Array2.filter(v => v > 10), v => v + 2)->
471+
*)
472+
match exp.pexp_desc with
473+
(* When the left side of the pipe we're completing is a function application.
474+
Example: someArray->Js.Array2.map(v => v + 2)-> *)
475+
| Pexp_apply
476+
( {pexp_desc = Pexp_ident {txt = Lident ("|." | "|.u")}},
477+
[_; (_, {pexp_desc = Pexp_apply (d, _)})] ) ->
478+
exprToContextPath exp |> Option.map (fun ctxPath -> (ctxPath, d.pexp_loc))
479+
(* When the left side of the pipe we're completing is an identifier application.
480+
Example: someArray->filterAllTheGoodStuff-> *)
481+
| Pexp_apply
482+
( {pexp_desc = Pexp_ident {txt = Lident ("|." | "|.u")}},
483+
[_; (_, {pexp_desc = Pexp_ident _; pexp_loc})] ) ->
484+
exprToContextPath exp |> Option.map (fun ctxPath -> (ctxPath, pexp_loc))
485+
| _ -> None
486+
487+
let completePipe ~id (lhs : Parsetree.expression) =
488+
match completePipeChain lhs with
489+
| Some (pipe, lhsLoc) -> Some (CPipe {ctxPath = pipe; id; lhsLoc})
490+
| None -> (
491+
match exprToContextPath lhs with
492+
| Some pipe -> Some (CPipe {ctxPath = pipe; id; lhsLoc = lhs.pexp_loc})
493+
| None -> None)
494+
440495
(** Scopes *)
441496
let rec scopePattern ~scope (pat : Parsetree.pattern) =
442497
match pat.ppat_desc with
@@ -554,40 +609,52 @@ and completeValueBinding ~(completionContext : CompletionContext.t)
554609
completionContext
555610
in
556611
completePattern ~completionContext vb.pvb_pat
557-
else if locHasPos vb.pvb_expr.pexp_loc then
612+
else if locHasPos vb.pvb_loc then
613+
(* First try completing the expression. *)
558614
(* A let binding expression either has the constraint of the binding,
559615
or an inferred constraint (if it has been compiled), or no constraint. *)
560-
let completionContext =
616+
let completionContextForExprCompletion =
561617
completionContext
562618
|> CompletionContext.currentlyExpectingOrTypeAtLoc
563619
~loc:vb.pvb_pat.ppat_loc bindingConstraint
564620
in
565-
completeExpr ~completionContext vb.pvb_expr
566-
else if locHasPos vb.pvb_loc then
567-
(* In the binding but not in the pattern or expression means parser error recovery.
568-
We can still complete the pattern or expression if we have enough information. *)
569-
let exprHole = checkIfExprHoleEmptyCursor ~completionContext vb.pvb_expr in
570-
let patHole = checkIfPatternHoleEmptyCursor ~completionContext vb.pvb_pat in
571-
let exprCtxPath = exprToContextPath vb.pvb_expr in
572-
(* Try the expression. Example: `let someVar: someType = <com> *)
573-
if exprHole then
574-
let completionContext =
575-
completionContext
576-
|> CompletionContext.currentlyExpectingOrTypeAtLoc
577-
~loc:vb.pvb_pat.ppat_loc bindingConstraint
621+
let completedExpression =
622+
completeExpr ~completionContext:completionContextForExprCompletion
623+
vb.pvb_expr
624+
in
625+
match completedExpression with
626+
| Some res -> Some res
627+
| None ->
628+
(* In the binding but not in the pattern or expression means parser error recovery.
629+
We can still complete the pattern or expression if we have enough information. *)
630+
let exprHole =
631+
checkIfExprHoleEmptyCursor
632+
~completionContext:completionContextForExprCompletion vb.pvb_expr
578633
in
579-
CompletionResult.ctxPath (CId ([], Value)) completionContext
580-
else if patHole then
581-
let completionContext =
582-
CompletionContext.currentlyExpectingOrTypeAtLoc
583-
~loc:vb.pvb_expr.pexp_loc
584-
(match exprCtxPath with
585-
| None -> None
586-
| Some ctxPath -> Some (Type ctxPath))
587-
completionContext
634+
let patHole =
635+
checkIfPatternHoleEmptyCursor
636+
~completionContext:completionContextForExprCompletion vb.pvb_pat
588637
in
589-
CompletionResult.pattern ~prefix:"" ~completionContext
590-
else None
638+
let exprCtxPath = exprToContextPath vb.pvb_expr in
639+
(* Try the expression. Example: `let someVar: someType = <com> *)
640+
if exprHole then
641+
let completionContext =
642+
completionContextForExprCompletion
643+
|> CompletionContext.currentlyExpectingOrTypeAtLoc
644+
~loc:vb.pvb_pat.ppat_loc bindingConstraint
645+
in
646+
CompletionResult.ctxPath (CId ([], Value)) completionContext
647+
else if patHole then
648+
let completionContext =
649+
CompletionContext.currentlyExpectingOrTypeAtLoc
650+
~loc:vb.pvb_expr.pexp_loc
651+
(match exprCtxPath with
652+
| None -> None
653+
| Some ctxPath -> Some (Type ctxPath))
654+
completionContextForExprCompletion
655+
in
656+
CompletionResult.pattern ~prefix:"" ~completionContext
657+
else None
591658
else None
592659

593660
and completeValueBindings ~(completionContext : CompletionContext.t)
@@ -605,6 +672,7 @@ and completeValueBindings ~(completionContext : CompletionContext.t)
605672
|> Utils.findMap (fun (vb : Parsetree.value_binding) ->
606673
completeValueBinding ~completionContext vb)
607674

675+
(** Completes an expression. Designed to run without pre-checking if the cursor is in the expression. *)
608676
and completeExpr ~completionContext (expr : Parsetree.expression) :
609677
CompletionResult.t =
610678
let locHasPos = completionContext.positionContext.locHasPos in
@@ -689,15 +757,14 @@ and completeExpr ~completionContext (expr : Parsetree.expression) :
689757
CompletionResult.ctxPath
690758
(CId (flattenLidCheckDot ~completionContext fieldName, Value))
691759
completionContext
692-
else if locHasPos fieldExpr.pexp_loc then
760+
else
693761
completeExpr
694762
~completionContext:
695763
(CompletionContext.addCtxPathItem
696764
(CRecordField
697765
{prefix = fieldName.txt |> Longident.last; seenFields})
698766
completionContext)
699-
fieldExpr
700-
else None)
767+
fieldExpr)
701768
in
702769
match fieldToComplete with
703770
| None -> (
@@ -795,13 +862,96 @@ and completeExpr ~completionContext (expr : Parsetree.expression) :
795862
else if locHasPos nextExpr.pexp_loc then
796863
completeExpr ~completionContext nextExpr
797864
else None
798-
| Pexp_apply (fnExpr, _args) ->
865+
(* == Pipes == *)
866+
| Pexp_apply
867+
( {pexp_desc = Pexp_ident {txt = Lident ("|." | "|.u"); loc = opLoc}},
868+
[
869+
(_, lhs);
870+
(_, {pexp_desc = Pexp_extension _; pexp_loc = {loc_ghost = true}});
871+
] )
872+
when locHasPos opLoc -> (
873+
(* Case foo-> when the parser adds a ghost expression to the rhs
874+
so the apply expression does not include the cursor *)
875+
match completePipe lhs ~id:"" with
876+
| None -> None
877+
| Some cpipe ->
878+
completionContext |> CompletionContext.withResetCtx
879+
|> CompletionResult.ctxPath cpipe)
880+
| Pexp_apply
881+
( {pexp_desc = Pexp_ident {txt = Lident ("|." | "|.u")}},
882+
[
883+
(_, lhs);
884+
(_, {pexp_desc = Pexp_ident {txt = Longident.Lident id; loc}});
885+
] )
886+
when locHasPos loc -> (
887+
(* foo->id *)
888+
match completePipe lhs ~id with
889+
| None -> None
890+
| Some cpipe ->
891+
completionContext |> CompletionContext.withResetCtx
892+
|> CompletionResult.ctxPath cpipe)
893+
| Pexp_apply
894+
( {pexp_desc = Pexp_ident {txt = Lident ("|." | "|.u"); loc = opLoc}},
895+
[(_, lhs); _] )
896+
when Loc.end_ opLoc = completionContext.positionContext.cursor -> (
897+
match completePipe lhs ~id:"" with
898+
| None -> None
899+
| Some cpipe ->
900+
completionContext |> CompletionContext.withResetCtx
901+
|> CompletionResult.ctxPath cpipe)
902+
| Pexp_apply ({pexp_desc = Pexp_ident {txt = Lident ("|." | "|.u")}}, [_; _])
903+
->
904+
(* Ignore any other pipe. *)
905+
None
906+
| Pexp_apply (fnExpr, args) -> (
799907
if locHasPos fnExpr.pexp_loc then
908+
(* someFn<com>(). Cursor in the function expression itself. *)
800909
completeExpr
801910
~completionContext:(CompletionContext.withResetCtx completionContext)
802911
fnExpr
803-
else (* TODO: Complete args. Pipes *)
804-
None
912+
else
913+
(* Check if the args has the cursor. *)
914+
(* Keep track of the positions of unlabelled arguments. *)
915+
let unlabelledArgPos = ref (-1) in
916+
let fnContextPath = exprToContextPath fnExpr in
917+
let argWithCursorInExpr =
918+
args
919+
|> List.find_opt
920+
(fun ((arg, argExpr) : Asttypes.arg_label * Parsetree.expression)
921+
->
922+
if arg = Nolabel then unlabelledArgPos := !unlabelledArgPos + 1;
923+
locHasPos argExpr.pexp_loc)
924+
in
925+
(* TODO: Complete labelled argument names, pipes *)
926+
let makeCompletionContextWithArgumentLabel argumentLabel
927+
~functionContextPath =
928+
completionContext |> CompletionContext.withResetCtx
929+
|> CompletionContext.currentlyExpectingOrReset
930+
(Some
931+
(Type (CFunctionArgument {functionContextPath; argumentLabel})))
932+
in
933+
match (argWithCursorInExpr, fnContextPath) with
934+
| None, _ -> None
935+
| Some (Nolabel, argExpr), Some functionContextPath ->
936+
let completionContext =
937+
makeCompletionContextWithArgumentLabel
938+
(Unlabelled {argumentPosition = !unlabelledArgPos})
939+
~functionContextPath
940+
in
941+
completeExpr ~completionContext argExpr
942+
| Some (Labelled label, argExpr), Some functionContextPath ->
943+
let completionContext =
944+
makeCompletionContextWithArgumentLabel (Labelled label)
945+
~functionContextPath
946+
in
947+
completeExpr ~completionContext argExpr
948+
| Some (Optional label, argExpr), Some functionContextPath ->
949+
let completionContext =
950+
makeCompletionContextWithArgumentLabel (Optional label)
951+
~functionContextPath
952+
in
953+
completeExpr ~completionContext argExpr
954+
| _ -> None)
805955
| Pexp_fun _ ->
806956
(* We've found a function definition, like `let whatever = (someStr: string) => {}` *)
807957
let rec loopFnExprs ~(completionContext : CompletionContext.t)

‎analysis/src/new-completions-todo.md

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
## Questions
2+
3+
- Figure out location for completeExpr - when do we need to check locs, and when not?

‎analysis/tests/src/CompletionNew.res

+20
Original file line numberDiff line numberDiff line change
@@ -104,3 +104,23 @@ type fn = (~name: string=?, string) => bool
104104

105105
// let [(true, [false, f])] = someArr
106106
// ^co2
107+
108+
// == Apply ==
109+
// let x = if true && f {None}
110+
// ^co2
111+
112+
// let x = someFunc(() => {let x = true; f})
113+
// ^co2
114+
115+
// let x = someFunc(~labelledArg=f)
116+
// ^co2
117+
118+
// let x = someFunc(~labelledArg=)
119+
// ^co2
120+
121+
// == Pipes ==
122+
// let x = foo->id
123+
// ^co2
124+
125+
// let x = foo->
126+
// ^co2

‎analysis/tests/src/expected/CompletionNew.res.txt

+28
Original file line numberDiff line numberDiff line change
@@ -138,3 +138,31 @@ Result: Cpattern: ctxPath: array->CTupleItem($1)->array, rootType: Type<CId(Valu
138138
Scope: 0 items
139139
Looking for type: Type<CId(Value)=someArr>
140140

141+
Complete2 src/CompletionNew.res 108:23
142+
Result: Cexpression: ctxPath: CId(Value)=f, rootType: Type<CFunctionArgument CId(Value)=&&($1)>
143+
Scope: 0 items
144+
Looking for type: Type<CFunctionArgument CId(Value)=&&($1)>
145+
146+
Complete2 src/CompletionNew.res 111:42
147+
Result: Cexpression: ctxPath: CId(Value)=f, rootType: FunctionReturnType<CFunctionArgument CId(Value)=someFunc($0)>
148+
Scope: 1 items
149+
Looking for type: FunctionReturnType<CFunctionArgument CId(Value)=someFunc($0)>
150+
151+
Complete2 src/CompletionNew.res 114:34
152+
Result: Cexpression: ctxPath: CId(Value)=f, rootType: Type<CFunctionArgument CId(Value)=someFunc(~labelledArg)>
153+
Scope: 0 items
154+
Looking for type: Type<CFunctionArgument CId(Value)=someFunc(~labelledArg)>
155+
156+
Complete2 src/CompletionNew.res 117:33
157+
No completions
158+
159+
Complete2 src/CompletionNew.res 121:17
160+
Result: CtxPath: (CId(Value)=foo)->id
161+
Scope: 0 items
162+
Looking for type: Unit
163+
164+
Complete2 src/CompletionNew.res 124:16
165+
Result: CtxPath: (CId(Value)=foo)->
166+
Scope: 0 items
167+
Looking for type: Unit
168+

0 commit comments

Comments
 (0)