Skip to content

Commit ed08a8c

Browse files
cknittclaude
andauthored
Complete list literal elements from the list's element type (#9263)
Why: Completion inside list{…} lost the element type: an annotation list<kind> became the context path Type[list], extract_type had no case for list<'a>, and the [::] chain of a literal was walked as constructor payloads. The analysis now has a list type, context path and nested path next to the array ones; a [::] head is an element, its tail the rest of the list, and the [] at its end, which holds the cursor in list{} and list{a, }, a new element. Example: type kind = First | Second let ys: list<kind> = list{First, Se} Before (completable): Cexpression Type[list]=Se->variantPayload::::($1), variantPayload::::($0) After (completable): Cexpression list<Type[kind]>=Se->list Before (completions): module Set After (completions): Second, module Set Review: Does `list{...xs, }` complete an element now? No: a trailing spread is the tail of the [::] chain, so the parser yields plain `xs` and no element position is left, as #9212 notes. Fixes #9212 Claude-Session: https://claude.ai/code/session_01Kbf3gJuq6rWZULJVY3ATmY Signed-off-by: Christoph Knittel <ck@cca.io> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent a72e4bf commit ed08a8c

9 files changed

Lines changed: 490 additions & 24 deletions

‎analysis/src/completion_back_end.ml‎

Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -968,6 +968,12 @@ and get_completions_for_context_path ~state ~debug ~full ~opens ~raw_opens ~pos
968968
Completion.create "array" ~env
969969
~kind:(Completion.Value (Ctype.newconstr Predef.path_array []));
970970
]
971+
| CPList None ->
972+
if Debug.verbose () then print_endline "[ctx_path]--> CPList (no payload)";
973+
[
974+
Completion.create "list" ~env
975+
~kind:(Completion.Value (Ctype.newconstr Predef.path_list []));
976+
]
971977
| CPDict None ->
972978
if Debug.verbose () then print_endline "[ctx_path]--> CPDict (no payload)";
973979
[
@@ -998,9 +1004,16 @@ and get_completions_for_context_path ~state ~debug ~full ~opens ~raw_opens ~pos
9981004
Completion.create "dummy" ~env
9991005
~kind:(Completion.Value (Ctype.newconstr Predef.path_dict []));
10001006
])
1001-
| CPArray (Some cp) -> (
1007+
| (CPArray (Some cp) | CPList (Some cp)) as context_path -> (
1008+
let is_list =
1009+
match context_path with
1010+
| CPList _ -> true
1011+
| _ -> false
1012+
in
10021013
if Debug.verbose () then
1003-
print_endline "[ctx_path]--> CPArray (with payload)";
1014+
print_endline
1015+
(if is_list then "[ctx_path]--> CPList (with payload)"
1016+
else "[ctx_path]--> CPArray (with payload)");
10041017
match mode with
10051018
| Regular -> (
10061019
match
@@ -1014,14 +1027,21 @@ and get_completions_for_context_path ~state ~debug ~full ~opens ~raw_opens ~pos
10141027
[
10151028
Completion.create "dummy" ~env
10161029
~kind:
1017-
(Completion.ExtractedType (Tarray (env, ExtractedType typ), `Type));
1030+
(Completion.ExtractedType
1031+
( (if is_list then Tlist (env, ExtractedType typ)
1032+
else Tarray (env, ExtractedType typ)),
1033+
`Type ));
10181034
])
10191035
| Pipe ->
1020-
(* Pipe completion with array just needs to know that it's an array, not
1021-
what inner type it has. *)
1036+
(* Pipe completion with an array or a list just needs to know which one
1037+
it is, not what inner type it has. *)
10221038
[
10231039
Completion.create "dummy" ~env
1024-
~kind:(Completion.Value (Ctype.newconstr Predef.path_array []));
1040+
~kind:
1041+
(Completion.Value
1042+
(Ctype.newconstr
1043+
(if is_list then Predef.path_list else Predef.path_array)
1044+
[]));
10251045
])
10261046
| CPOption cp -> (
10271047
if Debug.verbose () then print_endline "[ctx_path]--> CPOption";
@@ -1947,6 +1967,24 @@ let rec complete_typed_value ~raw_opens ~full ~state ~prefix ~completion_context
19471967
~env;
19481968
]
19491969
else []
1970+
| Tlist (env, typ) as extracted_type ->
1971+
if Debug.verbose () then print_endline "[complete_typed_value]--> Tlist";
1972+
if prefix = "" then
1973+
[
1974+
create "list{}" ~includes_snippets:true ~insert_text:"list{$0}"
1975+
~sort_text:"A"
1976+
~kind:
1977+
(match typ with
1978+
| ExtractedType _ ->
1979+
ExtractedType
1980+
( extracted_type,
1981+
match mode with
1982+
| Pattern _ -> `Type
1983+
| Expression -> `Value )
1984+
| TypeExpr typ -> Value (Ctype.newconstr Predef.path_list [typ]))
1985+
~env;
1986+
]
1987+
else []
19501988
| Tdict (env, typ) as extracted_type ->
19511989
if Debug.verbose () then print_endline "[complete_typed_value]--> Tdict";
19521990
if prefix = "" then

‎analysis/src/completion_expressions.ml‎

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,23 @@ let rec traverse_expr (exp : Parsetree.expression) ~expr_path ~pos
2020
traverse_expr expr ~expr_path ~pos ~first_char_before_cursor_no_white
2121
| Pexp_ident {txt = Lident txt} -> some_if_has_cursor (txt, expr_path)
2222
| Pexp_construct ({txt = Lident "()"}, _) -> some_if_has_cursor ("", expr_path)
23+
| Pexp_construct ({txt = Lident "::"}, {txt = [head; tail]})
24+
when loc_has_cursor exp.pexp_loc -> (
25+
(* A list literal is a chain of [::]: complete its head as an element,
26+
and its tail as the rest of the list. *)
27+
match
28+
head
29+
|> traverse_expr
30+
~expr_path:([Completable.NList] @ expr_path)
31+
~first_char_before_cursor_no_white ~pos
32+
with
33+
| Some _ as item_with_cursor -> item_with_cursor
34+
| None ->
35+
tail |> traverse_expr ~expr_path ~first_char_before_cursor_no_white ~pos)
36+
| Pexp_construct ({txt = Lident "[]"}, {txt = []}) ->
37+
(* The end of a list literal, which holds the cursor at a new element:
38+
`list{<com>}` or `list{a, <com>}` *)
39+
some_if_has_cursor ("", [Completable.NList] @ expr_path)
2340
| Pexp_construct ({txt = Lident txt}, {txt = []}) ->
2441
some_if_has_cursor (txt, expr_path)
2542
| Pexp_variant (label, {txt = []}) ->
@@ -47,10 +64,15 @@ let rec traverse_expr (exp : Parsetree.expression) ~expr_path ~pos
4764
`[None, <com>, None]`) *)
4865
Some ("", next_expr_path)
4966
| _ -> None)
50-
| Pexp_spread (Spread_array, elements) -> (
51-
(* Complete an item like an element of an array literal, and a spread
52-
operand like the whole literal, whose type it has. *)
53-
let next_expr_path = [Completable.NArray] @ expr_path in
67+
| Pexp_spread (kind, elements) -> (
68+
(* Complete an item like an element of the literal, and a spread operand
69+
like the whole literal, whose type it has. *)
70+
let item_path, opening =
71+
match kind with
72+
| Spread_array -> (Completable.NArray, '[')
73+
| Spread_list -> (NList, '{')
74+
in
75+
let next_expr_path = [item_path] @ expr_path in
5476
let item_with_cursor =
5577
elements
5678
|> List.find_map (function
@@ -66,7 +88,7 @@ let rec traverse_expr (exp : Parsetree.expression) ~expr_path ~pos
6688
| None
6789
when loc_has_cursor exp.pexp_loc
6890
&& (first_char_before_cursor_no_white = Some ','
69-
|| first_char_before_cursor_no_white = Some '[')
91+
|| first_char_before_cursor_no_white = Some opening)
7092
&& not
7193
(elements
7294
|> List.exists (function
@@ -75,10 +97,6 @@ let rec traverse_expr (exp : Parsetree.expression) ~expr_path ~pos
7597
(* A new item at the start or after a comma, e.g. `[...xs, <com>]` *)
7698
Some ("", next_expr_path)
7799
| None -> None)
78-
| Pexp_spread (Spread_list, _) ->
79-
(* Like a list literal without a spread, a chain of [::], which gets no
80-
typed completion either: the element type of a list is not extracted. *)
81-
None
82100
| Pexp_dict entries -> (
83101
(* Complete inside the value of the entry with the cursor, or for the type
84102
of the values at an expression hole, e.g. `dict{"a": <com>}` *)

‎analysis/src/shared_types.ml‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,7 @@ and completion_type =
341341
}
342342
| Tbool of Query_env.t
343343
| Tarray of Query_env.t * inner_type
344+
| Tlist of Query_env.t * inner_type
344345
| Tdict of Query_env.t * inner_type
345346
| Tstring of Query_env.t
346347
| TtypeT of {env: Query_env.t; path: Path.t}
@@ -582,6 +583,7 @@ module Completable = struct
582583
source_arity: int;
583584
}
584585
| NArray
586+
| NList (** An element of a list literal *)
585587
| NDictValue of {optional: bool}
586588
(** The pattern or expression of a dict entry, [?P] if [optional] *)
587589

@@ -597,11 +599,13 @@ module Completable = struct
597599
"polyvariantPayload::" ^ constructor_name ^ "($" ^ string_of_int item_num
598600
^ ")"
599601
| NArray -> "array"
602+
| NList -> "list"
600603
| NDictValue {optional} -> if optional then "dictValue(?)" else "dictValue"
601604

602605
type context_path =
603606
| CPString
604607
| CPArray of context_path option
608+
| CPList of context_path option
605609
| CPDict of context_path option
606610
| CPInt
607611
| CPFloat
@@ -704,6 +708,8 @@ module Completable = struct
704708
| CPArray (Some ctx_path) ->
705709
"array<" ^ context_path_to_string ctx_path ^ ">"
706710
| CPArray None -> "array"
711+
| CPList (Some ctx_path) -> "list<" ^ context_path_to_string ctx_path ^ ">"
712+
| CPList None -> "list"
707713
| CPDict (Some ctx_path) -> "dict<" ^ context_path_to_string ctx_path ^ ">"
708714
| CPDict None -> "dict"
709715
| CPId {path; completion_context} ->

‎analysis/src/type_utils.ml‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,10 @@ let rec extracted_type_to_string ?(name_only = false) ?(inner = false) =
103103
"array<" ^ Shared.type_to_string inner_typ ^ ">"
104104
| Tarray (_, ExtractedType inner_typ) ->
105105
"array<" ^ extracted_type_to_string ~inner:true inner_typ ^ ">"
106+
| Tlist (_, TypeExpr inner_typ) ->
107+
"list<" ^ Shared.type_to_string inner_typ ^ ">"
108+
| Tlist (_, ExtractedType inner_typ) ->
109+
"list<" ^ extracted_type_to_string ~inner:true inner_typ ^ ">"
106110
| Tdict (_, TypeExpr inner_typ) ->
107111
"dict<" ^ Shared.type_to_string inner_typ ^ ">"
108112
| Tdict (_, ExtractedType inner_typ) ->
@@ -351,6 +355,8 @@ let rec extract_type ?(print_opening_debug = true)
351355
Some (Tpromise (env, payload_type_expr), type_arg_context)
352356
| Tconstr (Path.Pident {name = "array"}, [payload_type_expr], _) ->
353357
Some (Tarray (env, TypeExpr payload_type_expr), type_arg_context)
358+
| Tconstr (Path.Pident {name = "list"}, [payload_type_expr], _) ->
359+
Some (Tlist (env, TypeExpr payload_type_expr), type_arg_context)
354360
| Tconstr (Path.Pident {name = "dict"}, [payload_type_expr], _) ->
355361
Some (Tdict (env, TypeExpr payload_type_expr), type_arg_context)
356362
| Tconstr (Path.Pident {name = "result"}, [ok_type; error_type], _) ->
@@ -786,9 +792,10 @@ let rec resolve_nested ?type_arg_context ~env ~full ~state ~nested ?ctx
786792
|> Utils.Option.flat_map (fun (typ, type_arg_context) ->
787793
typ |> resolve_nested ?type_arg_context ~env ~state ~full ~nested)
788794
))
789-
| NArray, Tarray (env, ExtractedType typ) ->
795+
| NArray, Tarray (env, ExtractedType typ)
796+
| NList, Tlist (env, ExtractedType typ) ->
790797
typ |> resolve_nested ?type_arg_context ~env ~state ~full ~nested
791-
| NArray, Tarray (env, TypeExpr typ) ->
798+
| NArray, Tarray (env, TypeExpr typ) | NList, Tlist (env, TypeExpr typ) ->
792799
typ
793800
|> extract_type ~env ~state ~package:full.package
794801
|> Utils.Option.flat_map (fun (typ, type_arg_context) ->
@@ -981,6 +988,8 @@ let rec context_path_from_core_type (core_type : Parsetree.core_type) =
981988
|> Option.map (fun inner_typ -> Completable.CPOption inner_typ)
982989
| Ptyp_constr ({txt = Lident "array"}, [inner_typ]) ->
983990
Some (Completable.CPArray (inner_typ |> context_path_from_core_type))
991+
| Ptyp_constr ({txt = Lident "list"}, [inner_typ]) ->
992+
Some (Completable.CPList (inner_typ |> context_path_from_core_type))
984993
| Ptyp_constr ({txt = Lident "dict"}, [inner_typ]) ->
985994
Some (Completable.CPDict (inner_typ |> context_path_from_core_type))
986995
| Ptyp_constr (lid, _) ->
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
- Editor: complete the elements of a list literal from the list's element type, like those of an array literal.
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
type kind = First | Second
2+
3+
let takesList = (xs: list<kind>) => ignore(xs)
4+
let takesNested = (xs: list<list<kind>>) => ignore(xs)
5+
6+
// let ys: list<kind> = list{First, }
7+
// ^com
8+
9+
// let ys: list<kind> = list{First, Se}
10+
// ^com
11+
12+
// let ys: list<kind> = list{}
13+
// ^com
14+
15+
// let ys: list<kind> = list{Fi, Second}
16+
// ^com
17+
18+
// let _ = takesList(list{})
19+
// ^com
20+
21+
// let _ = takesList(list{First, Se, Second})
22+
// ^com
23+
24+
// let _ = takesNested(list{list{First}, list{Se}})
25+
// ^com
26+
27+
// let ys: list<kind> =
28+
// ^com
29+
30+
// let ys: array<list<kind>> = [list{Fi}]
31+
// ^com

‎tests/analysis_tests/tests/src/CompletionSpreadLiteral.res‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,14 @@ let pair = (a: kind, b: kind) => [a, b]
4747
// let ys = [...base, Fi]
4848
// ^com
4949

50-
// A list literal gets no typed completion, with or without a spread
50+
// A list literal completes its elements like an array literal
5151
// let ys: list<kind> = list{...baseList, Fi, ...baseList}
5252
// ^com
53+
54+
// A trailing spread is the tail of a [::] chain, so `list{...baseList, }`
55+
// parses as `baseList`, and no element position is left
56+
// let ys: list<kind> = list{...baseList, }
57+
// ^com
58+
59+
// let ys: list<kind> = list{Fi, ...baseList}
60+
// ^com

0 commit comments

Comments
 (0)