From 8b099274cd73d1ac996f247a76fee00237a5fb57 Mon Sep 17 00:00:00 2001 From: Radek Simko Date: Wed, 25 Jan 2023 12:56:53 +0200 Subject: [PATCH 01/10] decoder: Implement completion for List --- decoder/expr_list.go | 5 -- decoder/expr_list_completion.go | 92 +++++++++++++++++++++++++++++++++ 2 files changed, 92 insertions(+), 5 deletions(-) create mode 100644 decoder/expr_list_completion.go diff --git a/decoder/expr_list.go b/decoder/expr_list.go index 28e58a0b..1580ecd0 100644 --- a/decoder/expr_list.go +++ b/decoder/expr_list.go @@ -15,11 +15,6 @@ type List struct { pathCtx *PathContext } -func (l List) CompletionAtPos(ctx context.Context, pos hcl.Pos) []lang.Candidate { - // TODO - return nil -} - func (l List) HoverAtPos(ctx context.Context, pos hcl.Pos) *lang.HoverData { // TODO return nil diff --git a/decoder/expr_list_completion.go b/decoder/expr_list_completion.go new file mode 100644 index 00000000..69c92faf --- /dev/null +++ b/decoder/expr_list_completion.go @@ -0,0 +1,92 @@ +package decoder + +import ( + "context" + "fmt" + "strings" + + "github.com/hashicorp/hcl-lang/lang" + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hclsyntax" +) + +func (list List) CompletionAtPos(ctx context.Context, pos hcl.Pos) []lang.Candidate { + if isEmptyExpression(list.expr) { + label := "[ ]" + triggerSuggest := false + + if list.cons.Elem != nil { + label = fmt.Sprintf("[ %s ]", list.cons.Elem.FriendlyName()) + triggerSuggest = true + } + + return []lang.Candidate{ + { + Label: label, + Detail: list.cons.FriendlyName(), + Kind: lang.ListCandidateKind, + Description: list.cons.Description, + TextEdit: lang.TextEdit{ + NewText: "[ ]", + Snippet: "[ ${0} ]", + Range: hcl.Range{ + Filename: list.expr.Range().Filename, + Start: pos, + End: pos, + }, + }, + TriggerSuggest: triggerSuggest, + }, + } + } + + eType, ok := list.expr.(*hclsyntax.TupleConsExpr) + if !ok { + return []lang.Candidate{} + } + + if list.cons.Elem == nil { + return []lang.Candidate{} + } + + fileBytes := list.pathCtx.Files[list.expr.Range().Filename].Bytes + + betweenBraces := hcl.Range{ + Filename: eType.Range().Filename, + Start: eType.OpenRange.End, + End: eType.Range().End, + } + + if betweenBraces.ContainsPos(pos) { + if len(eType.Exprs) == 0 { + expr := newEmptyExpressionAtPos(eType.Range().Filename, pos) + return newExpression(list.pathCtx, expr, list.cons.Elem).CompletionAtPos(ctx, pos) + } + + var lastElemEndPos hcl.Pos + for _, elemExpr := range eType.Exprs { + if elemExpr.Range().ContainsPos(pos) || elemExpr.Range().End.Byte == pos.Byte { + return newExpression(list.pathCtx, elemExpr, list.cons.Elem).CompletionAtPos(ctx, pos) + } + lastElemEndPos = elemExpr.Range().End + } + + rng := hcl.Range{ + Filename: eType.Range().Filename, + Start: lastElemEndPos, + End: pos, + } + + // TODO: test with multi-line element expressions + + b := rng.SliceBytes(fileBytes) + if strings.TrimSpace(string(b)) != "," { + return []lang.Candidate{} + } + + expr := newEmptyExpressionAtPos(eType.Range().Filename, pos) + return newExpression(list.pathCtx, expr, list.cons.Elem).CompletionAtPos(ctx, pos) + } + + return []lang.Candidate{} +} From 91289c48e0b31530b47ef645b59ef01c4fb505be Mon Sep 17 00:00:00 2001 From: Radek Simko Date: Wed, 25 Jan 2023 13:14:11 +0200 Subject: [PATCH 02/10] decoder: Implement hover for List --- decoder/expr_list.go | 5 ----- decoder/expr_list_hover.go | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 5 deletions(-) create mode 100644 decoder/expr_list_hover.go diff --git a/decoder/expr_list.go b/decoder/expr_list.go index 1580ecd0..db1f6eef 100644 --- a/decoder/expr_list.go +++ b/decoder/expr_list.go @@ -15,11 +15,6 @@ type List struct { pathCtx *PathContext } -func (l List) HoverAtPos(ctx context.Context, pos hcl.Pos) *lang.HoverData { - // TODO - return nil -} - func (l List) SemanticTokens(ctx context.Context) []lang.SemanticToken { // TODO return nil diff --git a/decoder/expr_list_hover.go b/decoder/expr_list_hover.go new file mode 100644 index 00000000..aace5a08 --- /dev/null +++ b/decoder/expr_list_hover.go @@ -0,0 +1,34 @@ +package decoder + +import ( + "context" + "fmt" + + "github.com/hashicorp/hcl-lang/lang" + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hclsyntax" +) + +func (list List) HoverAtPos(ctx context.Context, pos hcl.Pos) *lang.HoverData { + eType, ok := list.expr.(*hclsyntax.TupleConsExpr) + if !ok { + return nil + } + + for _, elemExpr := range eType.Exprs { + if elemExpr.Range().ContainsPos(pos) { + expr := newExpression(list.pathCtx, elemExpr, list.cons.Elem) + return expr.HoverAtPos(ctx, pos) + } + } + + content := fmt.Sprintf("_%s_", list.cons.FriendlyName()) + if list.cons.Description.Value != "" { + content += "\n\n" + list.cons.Description.Value + } + + return &lang.HoverData{ + Content: lang.Markdown(content), + Range: eType.Range(), + } +} From b355e443aba466c98001409521757556a2a035d8 Mon Sep 17 00:00:00 2001 From: Radek Simko Date: Wed, 25 Jan 2023 13:18:26 +0200 Subject: [PATCH 03/10] decoder: Implement semantic tokens for List --- decoder/expr_list.go | 6 ------ decoder/expr_list_semtok.go | 28 ++++++++++++++++++++++++++++ 2 files changed, 28 insertions(+), 6 deletions(-) create mode 100644 decoder/expr_list_semtok.go diff --git a/decoder/expr_list.go b/decoder/expr_list.go index db1f6eef..aa78a2f7 100644 --- a/decoder/expr_list.go +++ b/decoder/expr_list.go @@ -3,7 +3,6 @@ package decoder import ( "context" - "github.com/hashicorp/hcl-lang/lang" "github.com/hashicorp/hcl-lang/reference" "github.com/hashicorp/hcl-lang/schema" "github.com/hashicorp/hcl/v2" @@ -15,11 +14,6 @@ type List struct { pathCtx *PathContext } -func (l List) SemanticTokens(ctx context.Context) []lang.SemanticToken { - // TODO - return nil -} - func (l List) ReferenceOrigins(ctx context.Context, allowSelfRefs bool) reference.Origins { // TODO return nil diff --git a/decoder/expr_list_semtok.go b/decoder/expr_list_semtok.go new file mode 100644 index 00000000..71fc2dd1 --- /dev/null +++ b/decoder/expr_list_semtok.go @@ -0,0 +1,28 @@ +package decoder + +import ( + "context" + + "github.com/hashicorp/hcl-lang/lang" + "github.com/hashicorp/hcl/v2/hclsyntax" +) + +func (list List) SemanticTokens(ctx context.Context) []lang.SemanticToken { + eType, ok := list.expr.(*hclsyntax.TupleConsExpr) + if !ok { + return []lang.SemanticToken{} + } + + if len(eType.Exprs) == 0 || list.cons.Elem == nil { + return []lang.SemanticToken{} + } + + tokens := make([]lang.SemanticToken, 0) + + for _, elemExpr := range eType.Exprs { + expr := newExpression(list.pathCtx, elemExpr, list.cons.Elem) + tokens = append(tokens, expr.SemanticTokens(ctx)...) + } + + return tokens +} From 7761f39cc06720fb9d2a086da146d9567c31a11a Mon Sep 17 00:00:00 2001 From: Radek Simko Date: Wed, 25 Jan 2023 16:07:30 +0200 Subject: [PATCH 04/10] decoder: Implement reference origins for List --- decoder/expr_list.go | 5 ----- decoder/expr_list_ref_origins.go | 30 ++++++++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 5 deletions(-) create mode 100644 decoder/expr_list_ref_origins.go diff --git a/decoder/expr_list.go b/decoder/expr_list.go index aa78a2f7..94ba55e7 100644 --- a/decoder/expr_list.go +++ b/decoder/expr_list.go @@ -14,11 +14,6 @@ type List struct { pathCtx *PathContext } -func (l List) ReferenceOrigins(ctx context.Context, allowSelfRefs bool) reference.Origins { - // TODO - return nil -} - func (l List) ReferenceTargets(ctx context.Context, targetCtx *TargetContext) reference.Targets { // TODO return nil diff --git a/decoder/expr_list_ref_origins.go b/decoder/expr_list_ref_origins.go new file mode 100644 index 00000000..4787e5bb --- /dev/null +++ b/decoder/expr_list_ref_origins.go @@ -0,0 +1,30 @@ +package decoder + +import ( + "context" + + "github.com/hashicorp/hcl-lang/reference" + "github.com/hashicorp/hcl/v2/hclsyntax" +) + +func (list List) ReferenceOrigins(ctx context.Context, allowSelfRefs bool) reference.Origins { + eType, ok := list.expr.(*hclsyntax.TupleConsExpr) + if !ok { + return reference.Origins{} + } + + if len(eType.Exprs) == 0 || list.cons.Elem == nil { + return reference.Origins{} + } + + origins := make(reference.Origins, 0) + + for _, elemExpr := range eType.Exprs { + expr := newExpression(list.pathCtx, elemExpr, list.cons.Elem) + if e, ok := expr.(ReferenceOriginsExpression); ok { + origins = append(origins, e.ReferenceOrigins(ctx, allowSelfRefs)...) + } + } + + return origins +} From 8930e8db120f5f161d60d28fb4f8ea2f569b2fe9 Mon Sep 17 00:00:00 2001 From: Radek Simko Date: Wed, 25 Jan 2023 16:07:44 +0200 Subject: [PATCH 05/10] decoder: Implement reference targets for List --- decoder/expr_list.go | 8 ------ decoder/expr_list_ref_targets.go | 44 ++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 8 deletions(-) create mode 100644 decoder/expr_list_ref_targets.go diff --git a/decoder/expr_list.go b/decoder/expr_list.go index 94ba55e7..ef8abe74 100644 --- a/decoder/expr_list.go +++ b/decoder/expr_list.go @@ -1,9 +1,6 @@ package decoder import ( - "context" - - "github.com/hashicorp/hcl-lang/reference" "github.com/hashicorp/hcl-lang/schema" "github.com/hashicorp/hcl/v2" ) @@ -13,8 +10,3 @@ type List struct { cons schema.List pathCtx *PathContext } - -func (l List) ReferenceTargets(ctx context.Context, targetCtx *TargetContext) reference.Targets { - // TODO - return nil -} diff --git a/decoder/expr_list_ref_targets.go b/decoder/expr_list_ref_targets.go new file mode 100644 index 00000000..7494514c --- /dev/null +++ b/decoder/expr_list_ref_targets.go @@ -0,0 +1,44 @@ +package decoder + +import ( + "context" + + "github.com/hashicorp/hcl-lang/lang" + "github.com/hashicorp/hcl-lang/reference" + "github.com/hashicorp/hcl/v2/hclsyntax" + "github.com/zclconf/go-cty/cty" +) + +func (list List) ReferenceTargets(ctx context.Context, targetCtx *TargetContext) reference.Targets { + eType, ok := list.expr.(*hclsyntax.TupleConsExpr) + if !ok { + return reference.Targets{} + } + + if len(eType.Exprs) == 0 || list.cons.Elem == nil { + return reference.Targets{} + } + + targets := make(reference.Targets, 0) + + // TODO: collect parent target for the whole list + + for i, elemExpr := range eType.Exprs { + expr := newExpression(list.pathCtx, elemExpr, list.cons.Elem) + if e, ok := expr.(ReferenceTargetsExpression); ok { + elemCtx := targetCtx.Copy() + elemCtx.ParentAddress = append(elemCtx.ParentAddress, lang.IndexStep{ + Key: cty.NumberIntVal(int64(i)), + }) + if elemCtx.ParentLocalAddress != nil { + elemCtx.ParentLocalAddress = append(elemCtx.ParentLocalAddress, lang.IndexStep{ + Key: cty.NumberIntVal(int64(i)), + }) + } + + targets = append(targets, e.ReferenceTargets(ctx, elemCtx)...) + } + } + + return targets +} From 0707040154f57c420e4dad2fb40463d268802bd7 Mon Sep 17 00:00:00 2001 From: Daniel Banck Date: Thu, 23 Feb 2023 14:18:38 +0100 Subject: [PATCH 06/10] decoder: Test hover for List --- decoder/expr_list_hover_test.go | 309 ++++++++++++++++++++++++++++++++ 1 file changed, 309 insertions(+) create mode 100644 decoder/expr_list_hover_test.go diff --git a/decoder/expr_list_hover_test.go b/decoder/expr_list_hover_test.go new file mode 100644 index 00000000..f5faeaa7 --- /dev/null +++ b/decoder/expr_list_hover_test.go @@ -0,0 +1,309 @@ +package decoder + +import ( + "context" + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/hcl-lang/lang" + "github.com/hashicorp/hcl-lang/schema" + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hclsyntax" + "github.com/zclconf/go-cty/cty" +) + +func TestHoverAtPos_exprList(t *testing.T) { + testCases := []struct { + testName string + attrSchema map[string]*schema.AttributeSchema + cfg string + pos hcl.Pos + expectedHoverData *lang.HoverData + }{ + { + "empty single-line list without element", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.List{}, + }, + }, + `attr = []`, + hcl.Pos{Line: 1, Column: 9, Byte: 8}, + &lang.HoverData{ + Content: lang.Markdown("_list_"), + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + End: hcl.Pos{Line: 1, Column: 10, Byte: 9}, + }, + }, + }, + { + "empty multi-line list without element", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.List{}, + }, + }, + `attr = [ + +]`, + hcl.Pos{Line: 2, Column: 2, Byte: 10}, + &lang.HoverData{ + Content: lang.Markdown("_list_"), + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + End: hcl.Pos{Line: 3, Column: 2, Byte: 13}, + }, + }, + }, + { + "empty single-line list with element", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.List{ + Elem: schema.Keyword{ + Keyword: "keyword", + }, + }, + }, + }, + `attr = []`, + hcl.Pos{Line: 1, Column: 9, Byte: 8}, + &lang.HoverData{ + Content: lang.Markdown("_list of keyword_"), + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + End: hcl.Pos{Line: 1, Column: 10, Byte: 9}, + }, + }, + }, + { + "empty single-line list with element and description", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.List{ + Elem: schema.Keyword{ + Keyword: "keyword", + }, + Description: lang.Markdown("description"), + }, + }, + }, + `attr = []`, + hcl.Pos{Line: 1, Column: 9, Byte: 8}, + &lang.HoverData{ + Content: lang.Markdown("_list of keyword_\n\ndescription"), + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + End: hcl.Pos{Line: 1, Column: 10, Byte: 9}, + }, + }, + }, + { + "empty multi-line list with element", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.List{ + Elem: schema.Keyword{ + Keyword: "keyword", + }, + }, + }, + }, + `attr = [ + +]`, + hcl.Pos{Line: 2, Column: 2, Byte: 10}, + &lang.HoverData{ + Content: lang.Markdown("_list of keyword_"), + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + End: hcl.Pos{Line: 3, Column: 2, Byte: 13}, + }, + }, + }, + { + "single element single-line list on element", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.List{ + Elem: schema.Keyword{ + Keyword: "keyword", + }, + Description: lang.Markdown("description"), + }, + }, + }, + `attr = [keyword]`, + hcl.Pos{Line: 1, Column: 12, Byte: 11}, + &lang.HoverData{ + Content: lang.Markdown("`keyword` _keyword_"), + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 9, Byte: 8}, + End: hcl.Pos{Line: 1, Column: 16, Byte: 15}, + }, + }, + }, + { + "single element single-line list on element with custom data", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.List{ + Elem: schema.Keyword{ + Keyword: "keyword", + Description: lang.Markdown("key description"), + }, + Description: lang.Markdown("description"), + }, + }, + }, + `attr = [keyword]`, + hcl.Pos{Line: 1, Column: 12, Byte: 11}, + &lang.HoverData{ + Content: lang.Markdown("`keyword` _keyword_\n\nkey description"), + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 9, Byte: 8}, + End: hcl.Pos{Line: 1, Column: 16, Byte: 15}, + }, + }, + }, + { + "multi-element single-line list on list", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.List{ + Elem: schema.LiteralType{ + Type: cty.String, + }, + Description: lang.Markdown("description"), + }, + }, + }, + `attr = [ "one", "two" ]`, + hcl.Pos{Line: 1, Column: 8, Byte: 7}, + &lang.HoverData{ + Content: lang.Markdown("_list of string_\n\ndescription"), + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{ + Line: 1, + Column: 8, + Byte: 7, + }, + End: hcl.Pos{ + Line: 1, + Column: 24, + Byte: 23, + }, + }, + }, + }, + { + "single element multi-line list on element with custom data", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.List{ + Elem: schema.Keyword{ + Keyword: "keyword", + Description: lang.Markdown("key description"), + }, + Description: lang.Markdown("description"), + }, + }, + }, + `attr = [ + keyword, +]`, + hcl.Pos{Line: 2, Column: 6, Byte: 14}, + &lang.HoverData{ + Content: lang.Markdown("`keyword` _keyword_\n\nkey description"), + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 2, Column: 3, Byte: 11}, + End: hcl.Pos{Line: 2, Column: 10, Byte: 18}, + }, + }, + }, + { + "multi-element multi-line list on invalid element", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.List{ + Elem: schema.Keyword{ + Keyword: "keyword", + Description: lang.Markdown("key description"), + }, + Description: lang.Markdown("description"), + }, + }, + }, + `attr = [ + "foo", + keyword, +]`, + hcl.Pos{Line: 2, Column: 6, Byte: 14}, + nil, + }, + { + "multi-element multi-line list on second element", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.List{ + Elem: schema.Keyword{ + Keyword: "keyword", + Description: lang.Markdown("key description"), + }, + Description: lang.Markdown("description"), + }, + }, + }, + `attr = [ + keyword, + keyword, +]`, + hcl.Pos{Line: 3, Column: 6, Byte: 25}, + &lang.HoverData{ + Content: lang.Markdown("`keyword` _keyword_\n\nkey description"), + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 3, Column: 3, Byte: 22}, + End: hcl.Pos{Line: 3, Column: 10, Byte: 29}, + }, + }, + }, + } + + for i, tc := range testCases { + t.Run(fmt.Sprintf("%d-%s", i, tc.testName), func(t *testing.T) { + bodySchema := &schema.BodySchema{ + Attributes: tc.attrSchema, + } + + f, _ := hclsyntax.ParseConfig([]byte(tc.cfg), "test.tf", hcl.InitialPos) + d := testPathDecoder(t, &PathContext{ + Schema: bodySchema, + Files: map[string]*hcl.File{ + "test.tf": f, + }, + }) + + ctx := context.Background() + hoverData, err := d.HoverAtPos(ctx, "test.tf", tc.pos) + if err != nil { + t.Fatal(err) + } + + if diff := cmp.Diff(tc.expectedHoverData, hoverData); diff != "" { + t.Fatalf("unexpected hover data: %s", diff) + } + }) + } +} From 896eab470c529b90ff14cc4fc4f34fd8e0cd97e3 Mon Sep 17 00:00:00 2001 From: Daniel Banck Date: Thu, 23 Feb 2023 14:43:30 +0100 Subject: [PATCH 07/10] decoder: Test semantic tokens for List --- decoder/expr_list_semtok_test.go | 251 +++++++++++++++++++++++++++++++ 1 file changed, 251 insertions(+) create mode 100644 decoder/expr_list_semtok_test.go diff --git a/decoder/expr_list_semtok_test.go b/decoder/expr_list_semtok_test.go new file mode 100644 index 00000000..ba6e0b2d --- /dev/null +++ b/decoder/expr_list_semtok_test.go @@ -0,0 +1,251 @@ +package decoder + +import ( + "context" + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/hcl-lang/lang" + "github.com/hashicorp/hcl-lang/schema" + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hclsyntax" +) + +func TestSemanticTokens_exprList(t *testing.T) { + testCases := []struct { + testName string + attrSchema map[string]*schema.AttributeSchema + cfg string + expectedSemanticTokens []lang.SemanticToken + }{ + { + "empty list without element", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.List{}, + }, + }, + `attr = []`, + []lang.SemanticToken{ + { + Type: lang.TokenAttrName, + Modifiers: lang.SemanticTokenModifiers{}, + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 1, Column: 5, Byte: 4}, + }, + }, + }, + }, + { + "empty list with element", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.List{ + Elem: schema.Keyword{ + Keyword: "keyword", + }, + }, + }, + }, + `attr = []`, + []lang.SemanticToken{ + { + Type: lang.TokenAttrName, + Modifiers: lang.SemanticTokenModifiers{}, + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 1, Column: 5, Byte: 4}, + }, + }, + }, + }, + { + "single element list", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.List{ + Elem: schema.Keyword{ + Keyword: "keyword", + }, + }, + }, + }, + `attr = [keyword]`, + []lang.SemanticToken{ + { + Type: lang.TokenAttrName, + Modifiers: lang.SemanticTokenModifiers{}, + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 1, Column: 5, Byte: 4}, + }, + }, + { + Type: lang.TokenKeyword, + Modifiers: lang.SemanticTokenModifiers{}, + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 9, Byte: 8}, + End: hcl.Pos{Line: 1, Column: 16, Byte: 15}, + }, + }, + }, + }, + { + "single element multi-line list", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.List{ + Elem: schema.Keyword{ + Keyword: "keyword", + }, + }, + }, + }, + `attr = [ + keyword, +]`, + []lang.SemanticToken{ + { + Type: lang.TokenAttrName, + Modifiers: lang.SemanticTokenModifiers{}, + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 1, Column: 5, Byte: 4}, + }, + }, + { + Type: lang.TokenKeyword, + Modifiers: lang.SemanticTokenModifiers{}, + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 2, Column: 3, Byte: 11}, + End: hcl.Pos{Line: 2, Column: 10, Byte: 18}, + }, + }, + }, + }, + { + "multi-element multi-line list", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.List{ + Elem: schema.Keyword{ + Keyword: "keyword", + }, + }, + }, + }, + `attr = [ + keyword, + keyword, +]`, + []lang.SemanticToken{ + { + Type: lang.TokenAttrName, + Modifiers: lang.SemanticTokenModifiers{}, + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 1, Column: 5, Byte: 4}, + }, + }, + { + Type: lang.TokenKeyword, + Modifiers: lang.SemanticTokenModifiers{}, + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 2, Column: 3, Byte: 11}, + End: hcl.Pos{Line: 2, Column: 10, Byte: 18}, + }, + }, + { + Type: lang.TokenKeyword, + Modifiers: lang.SemanticTokenModifiers{}, + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 3, Column: 3, Byte: 22}, + End: hcl.Pos{Line: 3, Column: 10, Byte: 29}, + }, + }, + }, + }, + { + "multi-element multi-line list with invalid element", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.List{ + Elem: schema.Keyword{ + Keyword: "keyword", + }, + }, + }, + }, + `attr = [ + keyword, + invalid, + keyword, +]`, + []lang.SemanticToken{ + { + Type: lang.TokenAttrName, + Modifiers: lang.SemanticTokenModifiers{}, + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 1, Column: 5, Byte: 4}, + }, + }, + { + Type: lang.TokenKeyword, + Modifiers: lang.SemanticTokenModifiers{}, + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 2, Column: 3, Byte: 11}, + End: hcl.Pos{Line: 2, Column: 10, Byte: 18}, + }, + }, + { + Type: lang.TokenKeyword, + Modifiers: lang.SemanticTokenModifiers{}, + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 4, Column: 3, Byte: 33}, + End: hcl.Pos{Line: 4, Column: 10, Byte: 40}, + }, + }, + }, + }, + } + for i, tc := range testCases { + t.Run(fmt.Sprintf("%d-%s", i, tc.testName), func(t *testing.T) { + bodySchema := &schema.BodySchema{ + Attributes: tc.attrSchema, + } + + f, _ := hclsyntax.ParseConfig([]byte(tc.cfg), "test.tf", hcl.InitialPos) + d := testPathDecoder(t, &PathContext{ + Schema: bodySchema, + Files: map[string]*hcl.File{ + "test.tf": f, + }, + }) + + ctx := context.Background() + tokens, err := d.SemanticTokensInFile(ctx, "test.tf") + if err != nil { + t.Fatal(err) + } + + if diff := cmp.Diff(tc.expectedSemanticTokens, tokens); diff != "" { + t.Fatalf("unexpected tokens: %s", diff) + } + }) + } +} From 049061de84d6d9be412eb4df1518afc0b2f0cda4 Mon Sep 17 00:00:00 2001 From: Daniel Banck Date: Thu, 23 Feb 2023 16:46:18 +0100 Subject: [PATCH 08/10] decoder: Test completion for List --- decoder/expr_list_completion.go | 28 +- decoder/expr_list_completion_test.go | 686 +++++++++++++++++++++++++++ 2 files changed, 696 insertions(+), 18 deletions(-) create mode 100644 decoder/expr_list_completion_test.go diff --git a/decoder/expr_list_completion.go b/decoder/expr_list_completion.go index 69c92faf..4cc9d11a 100644 --- a/decoder/expr_list_completion.go +++ b/decoder/expr_list_completion.go @@ -3,7 +3,6 @@ package decoder import ( "context" "fmt" - "strings" "github.com/hashicorp/hcl-lang/lang" "github.com/hashicorp/hcl/v2" @@ -49,8 +48,6 @@ func (list List) CompletionAtPos(ctx context.Context, pos hcl.Pos) []lang.Candid return []lang.Candidate{} } - fileBytes := list.pathCtx.Files[list.expr.Range().Filename].Bytes - betweenBraces := hcl.Range{ Filename: eType.Range().Filename, Start: eType.OpenRange.End, @@ -63,25 +60,20 @@ func (list List) CompletionAtPos(ctx context.Context, pos hcl.Pos) []lang.Candid return newExpression(list.pathCtx, expr, list.cons.Elem).CompletionAtPos(ctx, pos) } - var lastElemEndPos hcl.Pos for _, elemExpr := range eType.Exprs { + // We cannot trust ranges of empty expressions, so we imply + // that invalid configuration follows and we stop here + // e.g. for completion between commas [keyword, ,keyword] + if isEmptyExpression(elemExpr) { + break + } + // We overshot the position and stop + if elemExpr.Range().Start.Byte > pos.Byte { + break + } if elemExpr.Range().ContainsPos(pos) || elemExpr.Range().End.Byte == pos.Byte { return newExpression(list.pathCtx, elemExpr, list.cons.Elem).CompletionAtPos(ctx, pos) } - lastElemEndPos = elemExpr.Range().End - } - - rng := hcl.Range{ - Filename: eType.Range().Filename, - Start: lastElemEndPos, - End: pos, - } - - // TODO: test with multi-line element expressions - - b := rng.SliceBytes(fileBytes) - if strings.TrimSpace(string(b)) != "," { - return []lang.Candidate{} } expr := newEmptyExpressionAtPos(eType.Range().Filename, pos) diff --git a/decoder/expr_list_completion_test.go b/decoder/expr_list_completion_test.go new file mode 100644 index 00000000..ede0dd56 --- /dev/null +++ b/decoder/expr_list_completion_test.go @@ -0,0 +1,686 @@ +package decoder + +import ( + "context" + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/hcl-lang/lang" + "github.com/hashicorp/hcl-lang/schema" + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hclsyntax" + "github.com/zclconf/go-cty/cty" +) + +func TestCompletionAtPos_exprList(t *testing.T) { + testCases := []struct { + testName string + attrSchema map[string]*schema.AttributeSchema + cfg string + pos hcl.Pos + expectedCandidates lang.Candidates + }{ + { + "attribute as list expression", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.List{ + Elem: schema.LiteralType{ + Type: cty.String, + }, + }, + }, + }, + ` +`, + hcl.Pos{Line: 1, Column: 1, Byte: 0}, + lang.CompleteCandidates([]lang.Candidate{ + { + Label: `attr`, + Detail: "list of string", + TextEdit: lang.TextEdit{ + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + }, + NewText: "attr", + Snippet: "attr = [ \"${1:value}\" ]", + }, + Kind: lang.AttributeCandidateKind, + TriggerSuggest: false, + }, + }), + }, + { + "empty expression no element", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.List{}, + }, + }, + `attr = +`, + hcl.Pos{Line: 1, Column: 8, Byte: 7}, + lang.CompleteCandidates([]lang.Candidate{ + { + Label: `[ ]`, + Detail: "list", + TextEdit: lang.TextEdit{ + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + End: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + }, + NewText: "[ ]", + Snippet: "[ ${0} ]", + }, + Kind: lang.ListCandidateKind, + }, + }), + }, + { + "empty expression with element", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.List{ + Elem: schema.Keyword{ + Keyword: "keyword", + }, + }, + }, + }, + `attr = +`, + hcl.Pos{Line: 1, Column: 8, Byte: 7}, + lang.CompleteCandidates([]lang.Candidate{ + { + Label: `[ keyword ]`, + Detail: "list of keyword", + TextEdit: lang.TextEdit{ + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + End: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + }, + NewText: "[ keyword ]", + Snippet: "[ ${0:keyword} ]", + }, + Kind: lang.ListCandidateKind, + TriggerSuggest: true, + }, + }), + }, + + // single line tests + { + "inside brackets single-line", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.List{ + Elem: schema.Keyword{ + Keyword: "keyword", + }, + }, + }, + }, + `attr = [ ] +`, + hcl.Pos{Line: 1, Column: 10, Byte: 9}, + lang.CompleteCandidates([]lang.Candidate{ + { + Label: `keyword`, + Detail: "keyword", + TextEdit: lang.TextEdit{ + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 10, Byte: 9}, + End: hcl.Pos{Line: 1, Column: 10, Byte: 9}, + }, + NewText: `keyword`, + Snippet: `keyword`, + }, + Kind: lang.KeywordCandidateKind, + }, + }), + }, + { + "inside single-line partial element near end", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.List{ + Elem: schema.Keyword{ + Keyword: "keyword", + }, + }, + }, + }, + `attr = [ key ] +`, + hcl.Pos{Line: 1, Column: 13, Byte: 12}, + lang.CompleteCandidates([]lang.Candidate{ + { + Label: `keyword`, + Detail: "keyword", + TextEdit: lang.TextEdit{ + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 10, Byte: 9}, + End: hcl.Pos{Line: 1, Column: 13, Byte: 12}, + }, + NewText: `keyword`, + Snippet: `keyword`, + }, + Kind: lang.KeywordCandidateKind, + }, + }), + }, + { + "inside single-line complete element in the middle", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.List{ + Elem: schema.Keyword{ + Keyword: "keyword", + }, + }, + }, + }, + `attr = [ keyword, ] +`, + hcl.Pos{Line: 1, Column: 13, Byte: 12}, + lang.CompleteCandidates([]lang.Candidate{ + { + Label: `keyword`, + Detail: "keyword", + TextEdit: lang.TextEdit{ + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 10, Byte: 9}, + End: hcl.Pos{Line: 1, Column: 17, Byte: 16}, + }, + NewText: `keyword`, + Snippet: `keyword`, + }, + Kind: lang.KeywordCandidateKind, + }, + }), + }, + { + "inside single-line after previous element with comma", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.List{ + Elem: schema.Keyword{ + Keyword: "keyword", + }, + }, + }, + }, + `attr = [ keyword, ] +`, + hcl.Pos{Line: 1, Column: 19, Byte: 18}, + lang.CompleteCandidates([]lang.Candidate{ + { + Label: `keyword`, + Detail: "keyword", + TextEdit: lang.TextEdit{ + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 19, Byte: 18}, + End: hcl.Pos{Line: 1, Column: 19, Byte: 18}, + }, + NewText: `keyword`, + Snippet: `keyword`, + }, + Kind: lang.KeywordCandidateKind, + }, + }), + }, + { + "inside single-line after previous element without comma", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.List{ + Elem: schema.Keyword{ + Keyword: "keyword", + }, + }, + }, + }, + `attr = [ keyword ] +`, + hcl.Pos{Line: 1, Column: 19, Byte: 18}, + lang.CompleteCandidates([]lang.Candidate{ + { + Label: `keyword`, + Detail: "keyword", + TextEdit: lang.TextEdit{ + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 19, Byte: 18}, + End: hcl.Pos{Line: 1, Column: 19, Byte: 18}, + }, + NewText: `keyword`, + Snippet: `keyword`, + }, + Kind: lang.KeywordCandidateKind, + }, + }), + }, + { + "inside single-line between elements with commas", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.List{ + Elem: schema.Keyword{ + Keyword: "keyword", + }, + }, + }, + }, + `attr = [ keyword, , keyword ] +`, + hcl.Pos{Line: 1, Column: 19, Byte: 18}, + lang.CompleteCandidates([]lang.Candidate{ + { + Label: `keyword`, + Detail: "keyword", + TextEdit: lang.TextEdit{ + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 19, Byte: 18}, + End: hcl.Pos{Line: 1, Column: 19, Byte: 18}, + }, + NewText: `keyword`, + Snippet: `keyword`, + }, + Kind: lang.KeywordCandidateKind, + }, + }), + }, + { + "inside single-line partial element one of", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.List{ + Elem: schema.OneOf{ + schema.Keyword{ + Keyword: "keyword", + }, + schema.Keyword{ + Keyword: "other", + }, + }, + }, + }, + }, + `attr = [ key ] +`, + hcl.Pos{Line: 1, Column: 8, Byte: 9}, + lang.CompleteCandidates([]lang.Candidate{ + { + Label: `keyword`, + Detail: "keyword", + TextEdit: lang.TextEdit{ + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 10, Byte: 9}, + End: hcl.Pos{Line: 1, Column: 13, Byte: 12}, + }, + NewText: `keyword`, + Snippet: `keyword`, + }, + Kind: lang.KeywordCandidateKind, + }, + { + Label: `other`, + Detail: "keyword", + TextEdit: lang.TextEdit{ + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 10, Byte: 9}, + End: hcl.Pos{Line: 1, Column: 13, Byte: 12}, + }, + NewText: `other`, + Snippet: `other`, + }, + Kind: lang.KeywordCandidateKind, + }, + }), + }, + + // multi line tests + { + "inside brackets multi-line", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.List{ + Elem: schema.Keyword{ + Keyword: "keyword", + }, + }, + }, + }, + `attr = [ + +] +`, + hcl.Pos{Line: 2, Column: 3, Byte: 11}, + lang.CompleteCandidates([]lang.Candidate{ + { + Label: `keyword`, + Detail: "keyword", + TextEdit: lang.TextEdit{ + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 2, Column: 3, Byte: 11}, + End: hcl.Pos{Line: 2, Column: 3, Byte: 11}, + }, + NewText: `keyword`, + Snippet: `keyword`, + }, + Kind: lang.KeywordCandidateKind, + }, + }), + }, + { + "inside multi-line partial element near end", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.List{ + Elem: schema.Keyword{ + Keyword: "keyword", + }, + }, + }, + }, + `attr = [ + key +] +`, + hcl.Pos{Line: 2, Column: 6, Byte: 14}, + lang.CompleteCandidates([]lang.Candidate{ + { + Label: `keyword`, + Detail: "keyword", + TextEdit: lang.TextEdit{ + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 2, Column: 3, Byte: 11}, + End: hcl.Pos{Line: 2, Column: 6, Byte: 14}, + }, + NewText: `keyword`, + Snippet: `keyword`, + }, + Kind: lang.KeywordCandidateKind, + }, + }), + }, + { + "inside multi-line complete element in the middle", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.List{ + Elem: schema.Keyword{ + Keyword: "keyword", + }, + }, + }, + }, + `attr = [ + keyword, +] +`, + hcl.Pos{Line: 2, Column: 6, Byte: 14}, + lang.CompleteCandidates([]lang.Candidate{ + { + Label: `keyword`, + Detail: "keyword", + TextEdit: lang.TextEdit{ + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 2, Column: 3, Byte: 11}, + End: hcl.Pos{Line: 2, Column: 10, Byte: 18}, + }, + NewText: `keyword`, + Snippet: `keyword`, + }, + Kind: lang.KeywordCandidateKind, + }, + }), + }, + { + "inside multi-line new line before existing element", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.List{ + Elem: schema.Keyword{ + Keyword: "keyword", + }, + }, + }, + }, + `attr = [ + + keyword, +] +`, + hcl.Pos{Line: 2, Column: 3, Byte: 11}, + lang.CompleteCandidates([]lang.Candidate{ + { + Label: `keyword`, + Detail: "keyword", + TextEdit: lang.TextEdit{ + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 2, Column: 3, Byte: 11}, + End: hcl.Pos{Line: 2, Column: 3, Byte: 11}, + }, + NewText: `keyword`, + Snippet: `keyword`, + }, + Kind: lang.KeywordCandidateKind, + }, + }), + }, + { + "inside multi-line partial element before existing element", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.List{ + Elem: schema.Keyword{ + Keyword: "keyword", + }, + }, + }, + }, + `attr = [ + key + keyword, +] +`, + hcl.Pos{Line: 2, Column: 6, Byte: 14}, + lang.CompleteCandidates([]lang.Candidate{ + { + Label: `keyword`, + Detail: "keyword", + TextEdit: lang.TextEdit{ + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 2, Column: 3, Byte: 11}, + End: hcl.Pos{Line: 2, Column: 6, Byte: 14}, + }, + NewText: `keyword`, + Snippet: `keyword`, + }, + Kind: lang.KeywordCandidateKind, + }, + }), + }, + { + "inside multi-line after previous element with comma", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.List{ + Elem: schema.Keyword{ + Keyword: "keyword", + }, + }, + }, + }, + `attr = [ + keyword, + +] +`, + hcl.Pos{Line: 3, Column: 3, Byte: 22}, + lang.CompleteCandidates([]lang.Candidate{ + { + Label: `keyword`, + Detail: "keyword", + TextEdit: lang.TextEdit{ + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 3, Column: 3, Byte: 22}, + End: hcl.Pos{Line: 3, Column: 3, Byte: 22}, + }, + NewText: `keyword`, + Snippet: `keyword`, + }, + Kind: lang.KeywordCandidateKind, + }, + }), + }, + { + "inside multi-line after previous element without comma", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.List{ + Elem: schema.Keyword{ + Keyword: "keyword", + }, + }, + }, + }, + `attr = [ + keyword + +] +`, + hcl.Pos{Line: 3, Column: 3, Byte: 21}, + lang.CompleteCandidates([]lang.Candidate{ + { + Label: `keyword`, + Detail: "keyword", + TextEdit: lang.TextEdit{ + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 3, Column: 3, Byte: 21}, + End: hcl.Pos{Line: 3, Column: 3, Byte: 21}, + }, + NewText: `keyword`, + Snippet: `keyword`, + }, + Kind: lang.KeywordCandidateKind, + }, + }), + }, + { + "inside multi-line between elements with commas", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.List{ + Elem: schema.Keyword{ + Keyword: "keyword", + }, + }, + }, + }, + `attr = [ + keyword, + + keyword, +] +`, + hcl.Pos{Line: 3, Column: 3, Byte: 22}, + lang.CompleteCandidates([]lang.Candidate{ + { + Label: `keyword`, + Detail: "keyword", + TextEdit: lang.TextEdit{ + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 3, Column: 3, Byte: 22}, + End: hcl.Pos{Line: 3, Column: 3, Byte: 22}, + }, + NewText: `keyword`, + Snippet: `keyword`, + }, + Kind: lang.KeywordCandidateKind, + }, + }), + }, + { + "inside multi-line after previous element with comma same line", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.List{ + Elem: schema.Keyword{ + Keyword: "keyword", + }, + }, + }, + }, + `attr = [ + keyword, +] +`, + hcl.Pos{Line: 2, Column: 12, Byte: 20}, + lang.CompleteCandidates([]lang.Candidate{ + { + Label: `keyword`, + Detail: "keyword", + TextEdit: lang.TextEdit{ + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 2, Column: 12, Byte: 20}, + End: hcl.Pos{Line: 2, Column: 12, Byte: 20}, + }, + NewText: `keyword`, + Snippet: `keyword`, + }, + Kind: lang.KeywordCandidateKind, + }, + }), + }, + } + + for i, tc := range testCases { + t.Run(fmt.Sprintf("%2d-%s", i, tc.testName), func(t *testing.T) { + bodySchema := &schema.BodySchema{ + Attributes: tc.attrSchema, + } + + f, _ := hclsyntax.ParseConfig([]byte(tc.cfg), "test.tf", hcl.InitialPos) + d := testPathDecoder(t, &PathContext{ + Schema: bodySchema, + Files: map[string]*hcl.File{ + "test.tf": f, + }, + }) + + ctx := context.Background() + candidates, err := d.CandidatesAtPos(ctx, "test.tf", tc.pos) + if err != nil { + t.Fatal(err) + } + + if diff := cmp.Diff(tc.expectedCandidates, candidates); diff != "" { + t.Logf("position: %#v in config: %s", tc.pos, tc.cfg) + t.Fatalf("unexpected candidates: %s", diff) + } + }) + } +} From 743feaf4061f529e411114f6fa4c70866451e1ff Mon Sep 17 00:00:00 2001 From: Daniel Banck Date: Thu, 9 Mar 2023 10:43:37 +0100 Subject: [PATCH 09/10] decoder: Use EmptyCompletionData for list --- decoder/expr_list_completion.go | 10 +++++----- decoder/expr_list_completion_test.go | 16 ++++++++-------- schema/constraint_list.go | 4 ++-- schema/constraint_test.go | 2 +- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/decoder/expr_list_completion.go b/decoder/expr_list_completion.go index 4cc9d11a..b3106945 100644 --- a/decoder/expr_list_completion.go +++ b/decoder/expr_list_completion.go @@ -12,13 +12,13 @@ import ( func (list List) CompletionAtPos(ctx context.Context, pos hcl.Pos) []lang.Candidate { if isEmptyExpression(list.expr) { label := "[ ]" - triggerSuggest := false if list.cons.Elem != nil { label = fmt.Sprintf("[ %s ]", list.cons.Elem.FriendlyName()) - triggerSuggest = true } + d := list.cons.EmptyCompletionData(1, 0) + return []lang.Candidate{ { Label: label, @@ -26,15 +26,15 @@ func (list List) CompletionAtPos(ctx context.Context, pos hcl.Pos) []lang.Candid Kind: lang.ListCandidateKind, Description: list.cons.Description, TextEdit: lang.TextEdit{ - NewText: "[ ]", - Snippet: "[ ${0} ]", + NewText: d.NewText, + Snippet: d.Snippet, Range: hcl.Range{ Filename: list.expr.Range().Filename, Start: pos, End: pos, }, }, - TriggerSuggest: triggerSuggest, + TriggerSuggest: d.TriggerSuggest, }, } } diff --git a/decoder/expr_list_completion_test.go b/decoder/expr_list_completion_test.go index ede0dd56..b5c1f06f 100644 --- a/decoder/expr_list_completion_test.go +++ b/decoder/expr_list_completion_test.go @@ -74,7 +74,7 @@ func TestCompletionAtPos_exprList(t *testing.T) { End: hcl.Pos{Line: 1, Column: 8, Byte: 7}, }, NewText: "[ ]", - Snippet: "[ ${0} ]", + Snippet: "[ ${1} ]", }, Kind: lang.ListCandidateKind, }, @@ -85,8 +85,8 @@ func TestCompletionAtPos_exprList(t *testing.T) { map[string]*schema.AttributeSchema{ "attr": { Constraint: schema.List{ - Elem: schema.Keyword{ - Keyword: "keyword", + Elem: schema.LiteralType{ + Type: cty.String, }, }, }, @@ -96,19 +96,19 @@ func TestCompletionAtPos_exprList(t *testing.T) { hcl.Pos{Line: 1, Column: 8, Byte: 7}, lang.CompleteCandidates([]lang.Candidate{ { - Label: `[ keyword ]`, - Detail: "list of keyword", + Label: `[ string ]`, + Detail: "list of string", TextEdit: lang.TextEdit{ Range: hcl.Range{ Filename: "test.tf", Start: hcl.Pos{Line: 1, Column: 8, Byte: 7}, End: hcl.Pos{Line: 1, Column: 8, Byte: 7}, }, - NewText: "[ keyword ]", - Snippet: "[ ${0:keyword} ]", + NewText: "[ \"value\" ]", + Snippet: "[ \"${1:value}\" ]", }, Kind: lang.ListCandidateKind, - TriggerSuggest: true, + TriggerSuggest: false, }, }), }, diff --git a/schema/constraint_list.go b/schema/constraint_list.go index 095490dc..7ea89a49 100644 --- a/schema/constraint_list.go +++ b/schema/constraint_list.go @@ -51,7 +51,7 @@ func (l List) Copy() Constraint { func (l List) EmptyCompletionData(nextPlaceholder int, nestingLevel int) CompletionData { if l.Elem == nil { return CompletionData{ - NewText: "[]", + NewText: "[ ]", Snippet: fmt.Sprintf("[ ${%d} ]", nextPlaceholder), NextPlaceholder: nextPlaceholder + 1, } @@ -60,7 +60,7 @@ func (l List) EmptyCompletionData(nextPlaceholder int, nestingLevel int) Complet elemData := l.Elem.EmptyCompletionData(nextPlaceholder, nestingLevel) if elemData.NewText == "" || elemData.Snippet == "" { return CompletionData{ - NewText: "[]", + NewText: "[ ]", Snippet: fmt.Sprintf("[ ${%d} ]", nextPlaceholder), TriggerSuggest: elemData.TriggerSuggest, NextPlaceholder: nextPlaceholder + 1, diff --git a/schema/constraint_test.go b/schema/constraint_test.go index f10d20e5..901c8324 100644 --- a/schema/constraint_test.go +++ b/schema/constraint_test.go @@ -615,7 +615,7 @@ STRING }, }, CompletionData{ - NewText: "[]", + NewText: "[ ]", Snippet: "[ ${1} ]", TriggerSuggest: true, NextPlaceholder: 2, From 3a89bdd5cae51237daabbabd9f702aa7f513832f Mon Sep 17 00:00:00 2001 From: Daniel Banck Date: Mon, 13 Mar 2023 12:35:38 +0100 Subject: [PATCH 10/10] Update decoder/expr_list_ref_targets.go Co-authored-by: Radek Simko --- decoder/expr_list_ref_targets.go | 1 + 1 file changed, 1 insertion(+) diff --git a/decoder/expr_list_ref_targets.go b/decoder/expr_list_ref_targets.go index 7494514c..a6db3dff 100644 --- a/decoder/expr_list_ref_targets.go +++ b/decoder/expr_list_ref_targets.go @@ -22,6 +22,7 @@ func (list List) ReferenceTargets(ctx context.Context, targetCtx *TargetContext) targets := make(reference.Targets, 0) // TODO: collect parent target for the whole list + // See https://github.com/hashicorp/hcl-lang/issues/228 for i, elemExpr := range eType.Exprs { expr := newExpression(list.pathCtx, elemExpr, list.cons.Elem)