Skip to content

Commit 4c8dce3

Browse files
committed
more work on completing records in exprs
1 parent f908477 commit 4c8dce3

File tree

4 files changed

+315
-8
lines changed

4 files changed

+315
-8
lines changed

analysis/src/CompletionFrontEnd.ml

Lines changed: 71 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,69 @@ let isPatternTuple pat =
1515
| Ppat_tuple _ -> true
1616
| _ -> false
1717

18-
let traverseExpr (exp : Parsetree.expression) ~exprPath ~pos =
18+
let rec traverseExpr (exp : Parsetree.expression) ~exprPath ~pos
19+
~firstCharBeforeCursorNoWhite =
1920
let someIfHasCursor v =
2021
if exp.pexp_loc |> CursorPosition.locHasCursor ~pos then Some v else None
2122
in
2223
match exp.pexp_desc with
24+
| Pexp_ident {txt = Lident txt} when Utils.hasBraces exp.pexp_attributes ->
25+
(* An ident with braces attribute corresponds to for example `{n}`.
26+
Looks like a record but is parsed as an ident with braces. *)
27+
someIfHasCursor (txt, [Completable.ERecordBody {seenFields = []}] @ exprPath)
2328
| Pexp_ident {txt = Lident txt} -> someIfHasCursor (txt, exprPath)
2429
| Pexp_construct ({txt = Lident "()"}, _) -> someIfHasCursor ("", exprPath)
2530
| Pexp_construct ({txt = Lident txt}, None) -> someIfHasCursor (txt, exprPath)
2631
| Pexp_variant (label, None) -> someIfHasCursor ("#" ^ label, exprPath)
2732
| Pexp_record ([], _) ->
2833
(* Empty fields means we're in a record body `{}`. Complete for the fields. *)
2934
someIfHasCursor ("", [Completable.ERecordBody {seenFields = []}] @ exprPath)
35+
| Pexp_record (fields, _) -> (
36+
let fieldWithCursor = ref None in
37+
let fieldWithExprHole = ref None in
38+
fields
39+
|> List.iter (fun (fname, exp) ->
40+
match
41+
( fname.Location.txt,
42+
exp.Parsetree.pexp_loc |> CursorPosition.classifyLoc ~pos )
43+
with
44+
| Longident.Lident fname, HasCursor ->
45+
fieldWithCursor := Some (fname, exp)
46+
| Lident fname, _ when isExprHole exp ->
47+
fieldWithExprHole := Some (fname, exp)
48+
| _ -> ());
49+
let seenFields =
50+
fields
51+
|> List.filter_map (fun (fieldName, _f) ->
52+
match fieldName with
53+
| {Location.txt = Longident.Lident fieldName} -> Some fieldName
54+
| _ -> None)
55+
in
56+
match (!fieldWithCursor, !fieldWithExprHole) with
57+
| Some (fname, f), _ | None, Some (fname, f) -> (
58+
match f.pexp_desc with
59+
| Pexp_extension ({txt = "rescript.exprhole"}, _) ->
60+
(* An expression hole means for example `{someField: <com>}`. We want to complete for the type of `someField`. *)
61+
someIfHasCursor
62+
("", [Completable.EFollowRecordField {fieldName = fname}] @ exprPath)
63+
| Pexp_ident {txt = Lident txt} ->
64+
(* A var means `{s}` or similar. Complete for fields. *)
65+
someIfHasCursor (txt, [Completable.ERecordBody {seenFields}] @ exprPath)
66+
| _ ->
67+
f
68+
|> traverseExpr ~firstCharBeforeCursorNoWhite ~pos
69+
~exprPath:
70+
([Completable.EFollowRecordField {fieldName = fname}] @ exprPath)
71+
)
72+
| None, None -> (
73+
(* Figure out if we're completing for a new field.
74+
If the cursor is inside of the record body, but no field has the cursor,
75+
and there's no pattern hole. Check the first char to the left of the cursor,
76+
ignoring white space. If that's a comma, we assume you're completing for a new field. *)
77+
match firstCharBeforeCursorNoWhite with
78+
| Some ',' ->
79+
someIfHasCursor ("", [Completable.ERecordBody {seenFields}] @ exprPath)
80+
| _ -> None))
3081
| _ -> None
3182

3283
type prop = {
@@ -42,8 +93,8 @@ type jsxProps = {
4293
childrenStart: (int * int) option;
4394
}
4495

45-
let findJsxPropsCompletable ~jsxProps ~endPos ~posBeforeCursor ~posAfterCompName
46-
=
96+
let findJsxPropsCompletable ~jsxProps ~endPos ~posBeforeCursor
97+
~firstCharBeforeCursorNoWhite ~posAfterCompName =
4798
let allLabels =
4899
List.fold_right
49100
(fun prop allLabels -> prop.name :: allLabels)
@@ -66,7 +117,10 @@ let findJsxPropsCompletable ~jsxProps ~endPos ~posBeforeCursor ~posAfterCompName
66117
None
67118
else if prop.exp.pexp_loc |> Loc.hasPos ~pos:posBeforeCursor then
68119
(* Cursor on expr assigned *)
69-
match traverseExpr prop.exp ~exprPath:[] ~pos:posBeforeCursor with
120+
match
121+
traverseExpr prop.exp ~exprPath:[] ~pos:posBeforeCursor
122+
~firstCharBeforeCursorNoWhite
123+
with
70124
| Some (prefix, nested) ->
71125
Some
72126
(CjsxPropValue
@@ -148,8 +202,8 @@ let extractJsxProps ~(compName : Longident.t Location.loc) ~args =
148202
args |> processProps ~acc:[]
149203

150204
let findArgCompletables ~(args : arg list) ~endPos ~posBeforeCursor
151-
~(contextPath : Completable.contextPath) ~posAfterFunExpr ~charBeforeCursor
152-
~isPipedExpr =
205+
~(contextPath : Completable.contextPath) ~posAfterFunExpr
206+
~firstCharBeforeCursorNoWhite ~charBeforeCursor ~isPipedExpr =
153207
let fnHasCursor =
154208
posAfterFunExpr <= posBeforeCursor && posBeforeCursor < endPos
155209
in
@@ -171,7 +225,10 @@ let findArgCompletables ~(args : arg list) ~endPos ~posBeforeCursor
171225
then Some (Completable.CnamedArg (contextPath, labelled.name, allNames))
172226
else if exp.pexp_loc |> Loc.hasPos ~pos:posBeforeCursor then
173227
(* Completing in the assignment of labelled argument *)
174-
match traverseExpr exp ~exprPath:[] ~pos:posBeforeCursor with
228+
match
229+
traverseExpr exp ~exprPath:[] ~pos:posBeforeCursor
230+
~firstCharBeforeCursorNoWhite
231+
with
175232
| None -> None
176233
| Some (prefix, nested) ->
177234
Some
@@ -196,7 +253,10 @@ let findArgCompletables ~(args : arg list) ~endPos ~posBeforeCursor
196253
if Res_parsetree_viewer.isTemplateLiteral exp then None
197254
else if exp.pexp_loc |> Loc.hasPos ~pos:posBeforeCursor then
198255
(* Completing in an unlabelled argument *)
199-
match traverseExpr exp ~pos:posBeforeCursor ~exprPath:[] with
256+
match
257+
traverseExpr exp ~pos:posBeforeCursor ~firstCharBeforeCursorNoWhite
258+
~exprPath:[]
259+
with
200260
| None -> None
201261
| Some (prefix, nested) ->
202262
Some
@@ -972,6 +1032,7 @@ let completionWithParser1 ~currentFile ~debug ~offset ~path ~posCursor ~text =
9721032
let jsxCompletable =
9731033
findJsxPropsCompletable ~jsxProps ~endPos:(Loc.end_ expr.pexp_loc)
9741034
~posBeforeCursor ~posAfterCompName:(Loc.end_ compName.loc)
1035+
~firstCharBeforeCursorNoWhite
9751036
in
9761037
if jsxCompletable <> None then setResultOpt jsxCompletable
9771038
else if compName.loc |> Loc.hasPos ~pos:posBeforeCursor then
@@ -1009,6 +1070,7 @@ let completionWithParser1 ~currentFile ~debug ~offset ~path ~posCursor ~text =
10091070
~endPos:(Loc.end_ expr.pexp_loc) ~posBeforeCursor
10101071
~posAfterFunExpr:(Loc.end_ funExpr.pexp_loc)
10111072
~charBeforeCursor ~isPipedExpr:true
1073+
~firstCharBeforeCursorNoWhite
10121074
| None -> None
10131075
in
10141076

@@ -1044,6 +1106,7 @@ let completionWithParser1 ~currentFile ~debug ~offset ~path ~posCursor ~text =
10441106
~endPos:(Loc.end_ expr.pexp_loc) ~posBeforeCursor
10451107
~posAfterFunExpr:(Loc.end_ funExpr.pexp_loc)
10461108
~charBeforeCursor ~isPipedExpr:false
1109+
~firstCharBeforeCursorNoWhite
10471110
| None -> None
10481111
in
10491112

analysis/src/Utils.ml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,3 +159,6 @@ let rec skipWhite text i =
159159
match text.[i] with
160160
| ' ' | '\n' | '\r' | '\t' -> skipWhite text (i - 1)
161161
| _ -> i
162+
163+
let hasBraces attributes =
164+
attributes |> List.exists (fun (loc, _) -> loc.Location.txt = "ns.braces")

analysis/tests/src/CompletionExpressions.res

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,44 @@ let f = Some([false])
33

44
// switch (s, f) { | }
55
// ^com
6+
7+
type otherRecord = {
8+
someField: int,
9+
otherField: string,
10+
}
11+
12+
type rec someRecord = {
13+
age: int,
14+
offline: bool,
15+
online: option<bool>,
16+
variant: someVariant,
17+
polyvariant: somePolyVariant,
18+
nested: option<otherRecord>,
19+
}
20+
and someVariant = One | Two | Three(int, string)
21+
and somePolyVariant = [#one | #two(bool) | #three(someRecord, bool)]
22+
23+
let fnTakingRecord = (r: someRecord) => {
24+
ignore(r)
25+
}
26+
27+
// let _ = fnTakingRecord({})
28+
// ^com
29+
30+
// let _ = fnTakingRecord({n})
31+
// ^com
32+
33+
// let _ = fnTakingRecord({offline: })
34+
// ^com
35+
36+
// let _ = fnTakingRecord({age: 123, })
37+
// ^com
38+
39+
// let _ = fnTakingRecord({age: 123, offline: true})
40+
// ^com
41+
42+
// let _ = fnTakingRecord({age: 123, nested: })
43+
// ^com
44+
45+
// let _ = fnTakingRecord({age: 123, nested: {}})
46+
// ^com

0 commit comments

Comments
 (0)