diff --git a/decoder/block_candidates.go b/decoder/block_candidates.go index e88cdeaf..6f94d758 100644 --- a/decoder/block_candidates.go +++ b/decoder/block_candidates.go @@ -9,7 +9,10 @@ import ( "github.com/hashicorp/hcl/v2" ) -func blockSchemaToCandidate(blockType string, block *schema.BlockSchema, rng hcl.Range) lang.Candidate { +// blockSchemaToCandidate generates a lang.Candidate used for auto-complete inside an editor from a BlockSchema. +// If `prefillRequiredFields` is `false`, it returns a snippet that does not expect any prefilled fields. +// If `prefillRequiredFields` is `true`, it returns a snippet that is compatiable with a list of prefilled fields from `generateRequiredFieldsSnippet` +func (d *Decoder) BlockSchemaToCandidate(blockType string, block *schema.BlockSchema, rng hcl.Range) lang.Candidate { triggerSuggest := false if len(block.Labels) > 0 { // We make some naive assumptions here for simplicity @@ -31,13 +34,14 @@ func blockSchemaToCandidate(blockType string, block *schema.BlockSchema, rng hcl Kind: lang.BlockCandidateKind, TextEdit: lang.TextEdit{ NewText: blockType, - Snippet: snippetForBlock(blockType, block), + Snippet: snippetForBlock(blockType, block, d.PrefillRequiredFields), Range: rng, }, TriggerSuggest: triggerSuggest, } } +// detailForBlock returns a `Detail` info string to display in an editor in a hover event func detailForBlock(block *schema.BlockSchema) string { detail := "Block" if block.Type != schema.BlockTypeNil { @@ -54,7 +58,40 @@ func detailForBlock(block *schema.BlockSchema) string { return strings.TrimSpace(detail) } -func snippetForBlock(blockType string, block *schema.BlockSchema) string { +// snippetForBlock takes a block and returns a formatted snippet for a user to complete inside an editor. +// If `prefillRequiredFields` is `false`, it returns a snippet that does not expect any prefilled fields. +// If `prefillRequiredFields` is `true`, it returns a snippet that is compatiable with a list of prefilled fields from `generateRequiredFieldsSnippet` +func snippetForBlock(blockType string, block *schema.BlockSchema, prefillRequiredFields bool) string { + if prefillRequiredFields { + labels := "" + + depKey := false + for _, l := range block.Labels { + if l.IsDepKey { + depKey = true + } + } + + if depKey { + for _, l := range block.Labels { + if l.IsDepKey { + labels += ` "${0}"` + } else { + labels += fmt.Sprintf(` "%s"`, l.Name) + } + } + return fmt.Sprintf("%s%s {\n}", blockType, labels) + } + + placeholder := 1 + for _, l := range block.Labels { + labels += fmt.Sprintf(` "${%d:%s}"`, placeholder, l.Name) + placeholder++ + } + + return fmt.Sprintf("%s%s {\n ${%d}\n}", blockType, labels, placeholder) + } + labels := "" placeholder := 1 diff --git a/decoder/body_candidates.go b/decoder/body_candidates.go index 2b844546..eaa7ab46 100644 --- a/decoder/body_candidates.go +++ b/decoder/body_candidates.go @@ -10,6 +10,7 @@ import ( "github.com/hashicorp/hcl/v2/hclsyntax" ) +// bodySchemaCandidates returns candidates for completion of fields inside a body or block. func (d *Decoder) bodySchemaCandidates(body *hclsyntax.Body, schema *schema.BodySchema, prefixRng, editRng hcl.Range) lang.Candidates { prefix, _ := d.bytesFromRange(prefixRng) @@ -71,7 +72,7 @@ func (d *Decoder) bodySchemaCandidates(body *hclsyntax.Body, schema *schema.Body return candidates } - candidates.List = append(candidates.List, blockSchemaToCandidate(bType, block, editRng)) + candidates.List = append(candidates.List, d.BlockSchemaToCandidate(bType, block, editRng)) count++ } diff --git a/decoder/candidates.go b/decoder/candidates.go index 1059c74f..7751196e 100644 --- a/decoder/candidates.go +++ b/decoder/candidates.go @@ -117,7 +117,7 @@ func (d *Decoder) candidatesAtPos(body *hclsyntax.Body, outerBodyRng hcl.Range, return lang.ZeroCandidates(), nil } - return d.labelCandidatesFromDependentSchema(i, bSchema.DependentBody, prefixRng, rng) + return d.labelCandidatesFromDependentSchema(i, bSchema.DependentBody, prefixRng, rng, block, bSchema.Labels) } } diff --git a/decoder/decoder.go b/decoder/decoder.go index 67d1fd2a..615a7c86 100644 --- a/decoder/decoder.go +++ b/decoder/decoder.go @@ -28,6 +28,9 @@ type Decoder struct { utmMedium string // utm_content parameter, e.g. documentHover or documentLink useUtmContent bool + + // PrefillRequiredFields enriches label-based completion candidates with required attributes and blocks + PrefillRequiredFields bool } type ReferenceTargetReader func() lang.ReferenceTargets diff --git a/decoder/expression_candidates.go b/decoder/expression_candidates.go index f0c470c7..a6e6f129 100644 --- a/decoder/expression_candidates.go +++ b/decoder/expression_candidates.go @@ -673,6 +673,59 @@ func snippetForExprContraints(placeholder uint, ec schema.ExprConstraints) strin return "" } +func snippetForExprContraint(placeholder uint, ec schema.ExprConstraints) string { + e := ExprConstraints(ec) + + // TODO: implement rest of these + if t, ok := e.LiteralType(); ok { + return snippetForLiteralType(placeholder, t) + } + // case schema.LiteralValue: + // if len(ec) == 1 { + // return snippetForLiteralValue(placeholder, et.Val) + // } + // return "" + if t, ok := e.TupleConsExpr(); ok { + ec := ExprConstraints(t.AnyElem) + if ec.HasKeywordsOnly() { + return "[ ${0} ]" + } + return "[\n ${0}\n]" + } + if t, ok := e.ListExpr(); ok { + ec := ExprConstraints(t.Elem) + if ec.HasKeywordsOnly() { + return "[ ${0} ]" + } + return "[\n ${0}\n]" + } + if t, ok := e.SetExpr(); ok { + ec := ExprConstraints(t.Elem) + if ec.HasKeywordsOnly() { + return "[ ${0} ]" + } + return "[\n ${0}\n]" + } + if t, ok := e.TupleExpr(); ok { + // TODO: multiple constraints? + ec := ExprConstraints(t.Elems[0]) + if ec.HasKeywordsOnly() { + return "[ ${0} ]" + } + return "[\n ${0}\n]" + } + if t, ok := e.MapExpr(); ok { + return fmt.Sprintf("{\n ${%d:name} = %s\n }", + placeholder, + snippetForExprContraints(placeholder+1, t.Elem)) + } + if _, ok := e.ObjectExpr(); ok { + return fmt.Sprintf("{\n ${%d}\n }", placeholder+1) + } + + return "" +} + type snippetGenerator struct { placeholder uint } diff --git a/decoder/label_candidates.go b/decoder/label_candidates.go index 5f56cefb..bdf25e86 100644 --- a/decoder/label_candidates.go +++ b/decoder/label_candidates.go @@ -2,15 +2,17 @@ package decoder import ( "encoding/json" + "fmt" "sort" "strings" "github.com/hashicorp/hcl-lang/lang" "github.com/hashicorp/hcl-lang/schema" "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hclsyntax" ) -func (d *Decoder) labelCandidatesFromDependentSchema(idx int, db map[schema.SchemaKey]*schema.BodySchema, prefixRng, editRng hcl.Range) (lang.Candidates, error) { +func (d *Decoder) labelCandidatesFromDependentSchema(idx int, db map[schema.SchemaKey]*schema.BodySchema, prefixRng, editRng hcl.Range, block *hclsyntax.Block, labelSchemas []*schema.LabelSchema) (lang.Candidates, error) { candidates := lang.NewCandidates() candidates.IsComplete = true count := 0 @@ -35,44 +37,56 @@ func (d *Decoder) labelCandidatesFromDependentSchema(idx int, db map[schema.Sche bodySchema := db[schemaKey] for _, label := range depKeys.Labels { - if label.Index == idx { - if len(prefix) > 0 && !strings.HasPrefix(label.Value, string(prefix)) { - continue - } + if label.Index != idx { + continue + } - // Dependent keys may be duplicated where one - // key is labels-only and other one contains - // labels + attributes. - // - // Specifically in Terraform this applies to - // a resource type depending on 'provider' attribute. - // - // We do need such dependent keys elsewhere - // to know how to do completion within a block - // but this doesn't matter when completing the label itself - // unless/until we're also completing the dependent attributes. - if _, ok := foundCandidateNames[label.Value]; ok { - continue - } + if len(prefix) > 0 && !strings.HasPrefix(label.Value, string(prefix)) { + continue + } - candidates.List = append(candidates.List, lang.Candidate{ - Label: label.Value, - Kind: lang.LabelCandidateKind, - IsDeprecated: bodySchema.IsDeprecated, - TextEdit: lang.TextEdit{ - NewText: label.Value, - Snippet: label.Value, - Range: editRng, - }, - // TODO: AdditionalTextEdits: - // - prefill required fields if body is empty - // - prefill dependent attribute(s) - Detail: bodySchema.Detail, - Description: bodySchema.Description, - }) - foundCandidateNames[label.Value] = true - count++ + // Dependent keys may be duplicated where one + // key is labels-only and other one contains + // labels + attributes. + // + // Specifically in Terraform this applies to + // a resource type depending on 'provider' attribute. + // + // We do need such dependent keys elsewhere + // to know how to do completion within a block + // but this doesn't matter when completing the label itself + // unless/until we're also completing the dependent attributes. + if _, ok := foundCandidateNames[label.Value]; ok { + continue } + + te := lang.TextEdit{} + if d.PrefillRequiredFields { + snippet := generateRequiredFieldsSnippet(label.Value, bodySchema, labelSchemas, 2, 0) + te = lang.TextEdit{ + NewText: label.Value, + Snippet: snippet, + Range: hcl.RangeBetween(editRng, block.OpenBraceRange), + } + } else { + te = lang.TextEdit{ + NewText: label.Value, + Snippet: label.Value, + Range: editRng, + } + } + + candidates.List = append(candidates.List, lang.Candidate{ + Label: label.Value, + Kind: lang.LabelCandidateKind, + IsDeprecated: bodySchema.IsDeprecated, + TextEdit: te, + Detail: bodySchema.Detail, + Description: bodySchema.Description, + }) + + foundCandidateNames[label.Value] = true + count++ } } @@ -81,6 +95,115 @@ func (d *Decoder) labelCandidatesFromDependentSchema(idx int, db map[schema.Sche return candidates, nil } +// generateRequiredFieldsSnippet returns a properly formatted snippet of all required +// fields (attributes, blocks, etc). It handles the main stanza declaration and calls +// `requiredFieldsSnippet` to handle recursing through the body schema +func generateRequiredFieldsSnippet(label string, bodySchema *schema.BodySchema, labelSchemas []*schema.LabelSchema, placeholder int, indentCount int) string { + snippetText := "" + + // build a space deliminated string of dependent labels + // In Terraform, `label` is the resource we're printing, each label after is the dependent labels + // for example: resource "aws_instance" "foo" + if len(labelSchemas) > 0 { + snippetText += fmt.Sprintf("%s\"", label) + for _, l := range labelSchemas[1:] { + snippetText += fmt.Sprintf(" \"${%d:%s}\"", placeholder, l.Name) + placeholder++ + } + + // must end with a newline to have a correctly formated stanza + snippetText += " {\n" + } + + // get all required fields and build final snippet + snippetText += requiredFieldsSnippet(bodySchema, placeholder, indentCount) + + // add a final tabstop so that the user is landed in the correct place when + // they are finished tabbing through each field + snippetText += "\t${0}" + + return snippetText +} + +// requiredFieldsSnippet returns a properly formatted snippet of all required +// fields (attributes, blocks). It recurses through the Body schema to +// ensure nested fields are accounted for. It takes care to add newlines and +// tabs where necessary to have a snippet be formatted correctly in the target client +func requiredFieldsSnippet(bodySchema *schema.BodySchema, placeholder int, indentCount int) string { + // there are edge cases where we might not have a body, end early here + if bodySchema == nil { + return "" + } + + snippetText := "" + + // to handle recursion we check the value here. if its 0, this is the + // first call so we set to 0 to set a reasonable starting indent + if indentCount == 0 { + indentCount = 1 + } + indent := strings.Repeat("\t", indentCount) + + // store how many required attributes there are for the current body + reqAttr := 0 + attrNames := bodySchema.AttributeNames() + for _, attrName := range attrNames { + attr := bodySchema.Attributes[attrName] + if attr.IsRequired { + reqAttr++ + } + } + + // iterate over each attribute, skip if not required, and print snippet + attrCount := 0 + for _, attrName := range attrNames { + attr := bodySchema.Attributes[attrName] + if !attr.IsRequired { + continue + } + + valueSnippet := snippetForExprContraint(uint(placeholder), attr.Expr) + snippetText += fmt.Sprintf("%s%s = %s", indent, attrName, valueSnippet) + + // attrCount is used to tell if we are at the end of the list of attributes + // so we don't add a trailing newline. this will affect both attribute + // and block placement + attrCount++ + if attrCount <= reqAttr { + snippetText += "\n" + } + placeholder++ + } + + // iterate over each block, skip if not required, and print snippet + blockTypes := bodySchema.BlockTypes() + for _, blockType := range blockTypes { + blockSchema := bodySchema.Blocks[blockType] + if blockSchema.MinItems <= 0 { + continue + } + + // build a space deliminated string of dependent labels, if any + labels := "" + if len(blockSchema.Labels) > 0 { + for _, label := range blockSchema.Labels { + labels += fmt.Sprintf(` "${%d:%s}"`, placeholder, label.Name) + placeholder++ + } + } + + // newlines and indents here affect final snippet, be careful modifying order here + snippetText += fmt.Sprintf("%s%s%s {\n", indent, blockType, labels) + // we increment indentCount by 1 to indicate these are nested underneath + // recurse through the body to find any attributes or blocks and print snippet + snippetText += requiredFieldsSnippet(blockSchema.Body, placeholder, indentCount+1) + // final newline is needed here to properly format each block + snippetText += fmt.Sprintf("%s}\n", indent) + } + + return snippetText +} + func sortedSchemaKeys(m map[schema.SchemaKey]*schema.BodySchema) []schema.SchemaKey { keys := make([]schema.SchemaKey, 0) for k := range m { diff --git a/decoder/label_candidates_test.go b/decoder/label_candidates_test.go index 8ff77030..00cc078e 100644 --- a/decoder/label_candidates_test.go +++ b/decoder/label_candidates_test.go @@ -102,3 +102,526 @@ func TestDecoder_CandidateAtPos_incompleteLabels(t *testing.T) { t.Fatalf("unexpected candidates: %s", diff) } } + +func TestCandidatesAtPos_prefillRequiredFields(t *testing.T) { + startingConfig := "resource \"\" {\n}" + startingPos := hcl.Pos{ + Line: 1, + Column: 11, + Byte: 10, + } + wantRange := hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{ + Line: 1, + Column: 11, + Byte: 10, + }, + End: hcl.Pos{ + Line: 1, + Column: 14, + Byte: 13, + }, + } + tests := []struct { + name string + prefill bool + schema *schema.BodySchema + want lang.Candidates + }{ + { + name: "one dependent label no attributes or blocks", + prefill: true, + schema: &schema.BodySchema{ + Blocks: map[string]*schema.BlockSchema{ + "resource": { + Labels: []*schema.LabelSchema{ + { + Name: "type", + IsDepKey: true, + Completable: true, + }, + { + Name: "name", + }, + }, + DependentBody: map[schema.SchemaKey]*schema.BodySchema{ + schema.NewSchemaKey(schema.DependencyKeys{ + Labels: []schema.LabelDependent{ + {Index: 0, Value: "aws_appmesh_route"}, + }, + }): { + Attributes: map[string]*schema.AttributeSchema{ + "name": { + Expr: schema.LiteralTypeOnly(cty.String), + IsRequired: false, + }, + }, + }, + }, + }, + }, + }, + want: lang.CompleteCandidates([]lang.Candidate{ + { + Label: "aws_appmesh_route", + Kind: lang.LabelCandidateKind, + TextEdit: lang.TextEdit{ + Range: wantRange, + NewText: `aws_appmesh_route`, + Snippet: `aws_appmesh_route" "${2:name}" { + ${0}`, + }, + }, + }), + }, + { + name: "one dependent label one required attributes no blocks", + prefill: true, + schema: &schema.BodySchema{ + Blocks: map[string]*schema.BlockSchema{ + "resource": { + Labels: []*schema.LabelSchema{ + { + Name: "type", + IsDepKey: true, + Completable: true, + }, + { + Name: "name", + }, + }, + DependentBody: map[schema.SchemaKey]*schema.BodySchema{ + schema.NewSchemaKey(schema.DependencyKeys{ + Labels: []schema.LabelDependent{ + {Index: 0, Value: "aws_appmesh_route"}, + }, + }): { + Attributes: map[string]*schema.AttributeSchema{ + "foo": { + Expr: schema.LiteralTypeOnly(cty.String), + IsRequired: true, + }, + }, + }, + }, + }, + }, + }, + want: lang.CompleteCandidates([]lang.Candidate{ + { + Label: "aws_appmesh_route", + Kind: lang.LabelCandidateKind, + TextEdit: lang.TextEdit{ + Range: wantRange, + NewText: `aws_appmesh_route`, + Snippet: `aws_appmesh_route" "${2:name}" { + foo = "${3:value}" + ${0}`, + }, + }, + }), + }, + { + name: "one dependent label multiple required attributes one required block", + prefill: true, + schema: &schema.BodySchema{ + Blocks: map[string]*schema.BlockSchema{ + "resource": { + Labels: []*schema.LabelSchema{ + { + Name: "type", + IsDepKey: true, + Completable: true, + }, + { + Name: "name", + }, + }, + DependentBody: map[schema.SchemaKey]*schema.BodySchema{ + schema.NewSchemaKey(schema.DependencyKeys{ + Labels: []schema.LabelDependent{ + {Index: 0, Value: "aws_appmesh_route"}, + }, + }): { + Attributes: map[string]*schema.AttributeSchema{ + "name": { + Expr: schema.LiteralTypeOnly(cty.String), + IsRequired: true, + }, + "anothername": { + Expr: schema.LiteralTypeOnly(cty.String), + IsRequired: true, + }, + }, + Blocks: map[string]*schema.BlockSchema{ + "spec": { + Type: schema.BlockTypeMap, + MinItems: 1, + }, + }, + }, + }, + }, + }, + }, + want: lang.CompleteCandidates([]lang.Candidate{ + { + Label: "aws_appmesh_route", + Kind: lang.LabelCandidateKind, + TextEdit: lang.TextEdit{ + Range: wantRange, + NewText: `aws_appmesh_route`, + Snippet: `aws_appmesh_route" "${2:name}" { + anothername = "${3:value}" + name = "${4:value}" + spec { + } + ${0}`, + }, + }, + }), + }, + { + name: "one dependent label multiple required attributes one required block with required attribute", + prefill: true, + schema: &schema.BodySchema{ + Blocks: map[string]*schema.BlockSchema{ + "resource": { + Labels: []*schema.LabelSchema{ + { + Name: "type", + IsDepKey: true, + Completable: true, + }, + { + Name: "name", + }, + }, + DependentBody: map[schema.SchemaKey]*schema.BodySchema{ + schema.NewSchemaKey(schema.DependencyKeys{ + Labels: []schema.LabelDependent{ + {Index: 0, Value: "aws_appmesh_route"}, + }, + }): { + Attributes: map[string]*schema.AttributeSchema{ + "name": { + Expr: schema.LiteralTypeOnly(cty.String), + IsRequired: true, + }, + "anothername": { + Expr: schema.LiteralTypeOnly(cty.String), + IsRequired: true, + }, + }, + Blocks: map[string]*schema.BlockSchema{ + "spec": { + Type: schema.BlockTypeMap, + MinItems: 1, + Body: &schema.BodySchema{ + Attributes: map[string]*schema.AttributeSchema{ + "name": { + Expr: schema.LiteralTypeOnly(cty.String), + IsRequired: true, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + want: lang.CompleteCandidates([]lang.Candidate{ + { + Label: "aws_appmesh_route", + Kind: lang.LabelCandidateKind, + TextEdit: lang.TextEdit{ + Range: wantRange, + NewText: `aws_appmesh_route`, + Snippet: `aws_appmesh_route" "${2:name}" { + anothername = "${3:value}" + name = "${4:value}" + spec { + name = "${5:value}" + } + ${0}`, + }, + }, + }), + }, + { + name: "one dependent label multiple required attributes one required block with dependent label with no required attributes", + prefill: true, + schema: &schema.BodySchema{ + Blocks: map[string]*schema.BlockSchema{ + "resource": { + Labels: []*schema.LabelSchema{ + { + Name: "type", + IsDepKey: true, + Completable: true, + }, + { + Name: "name", + }, + }, + DependentBody: map[schema.SchemaKey]*schema.BodySchema{ + schema.NewSchemaKey(schema.DependencyKeys{ + Labels: []schema.LabelDependent{ + {Index: 0, Value: "aws_appmesh_route"}, + }, + }): { + Attributes: map[string]*schema.AttributeSchema{ + "name": { + Expr: schema.LiteralTypeOnly(cty.String), + IsRequired: true, + }, + "anothername": { + Expr: schema.LiteralTypeOnly(cty.String), + IsRequired: true, + }, + }, + Blocks: map[string]*schema.BlockSchema{ + "spec": { + Type: schema.BlockTypeMap, + Labels: []*schema.LabelSchema{ + { + Name: "key", + }, + }, + MinItems: 1, + Body: &schema.BodySchema{ + Attributes: map[string]*schema.AttributeSchema{ + "name": { + Expr: schema.LiteralTypeOnly(cty.String), + IsRequired: false, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + want: lang.CompleteCandidates([]lang.Candidate{ + { + Label: "aws_appmesh_route", + Kind: lang.LabelCandidateKind, + TextEdit: lang.TextEdit{ + Range: wantRange, + NewText: `aws_appmesh_route`, + Snippet: `aws_appmesh_route" "${2:name}" { + anothername = "${3:value}" + name = "${4:value}" + spec "${5:key}" { + } + ${0}`, + }, + }, + }), + }, + { + name: "one dependent label multiple required attributes one required block with dependent label with required attribute", + prefill: true, + schema: &schema.BodySchema{ + Blocks: map[string]*schema.BlockSchema{ + "resource": { + Labels: []*schema.LabelSchema{ + { + Name: "type", + IsDepKey: true, + Completable: true, + }, + { + Name: "name", + }, + }, + DependentBody: map[schema.SchemaKey]*schema.BodySchema{ + schema.NewSchemaKey(schema.DependencyKeys{ + Labels: []schema.LabelDependent{ + {Index: 0, Value: "aws_appmesh_route"}, + }, + }): { + Attributes: map[string]*schema.AttributeSchema{ + "name": { + Expr: schema.LiteralTypeOnly(cty.String), + IsRequired: true, + }, + "anothername": { + Expr: schema.LiteralTypeOnly(cty.String), + IsRequired: true, + }, + }, + Blocks: map[string]*schema.BlockSchema{ + "spec": { + Type: schema.BlockTypeMap, + Labels: []*schema.LabelSchema{ + { + Name: "key", + }, + }, + MinItems: 1, + Body: &schema.BodySchema{ + Attributes: map[string]*schema.AttributeSchema{ + "name": { + Expr: schema.LiteralTypeOnly(cty.String), + IsRequired: true, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + want: lang.CompleteCandidates([]lang.Candidate{ + { + Label: "aws_appmesh_route", + Kind: lang.LabelCandidateKind, + TextEdit: lang.TextEdit{ + Range: wantRange, + NewText: `aws_appmesh_route`, + Snippet: `aws_appmesh_route" "${2:name}" { + anothername = "${3:value}" + name = "${4:value}" + spec "${5:key}" { + name = "${6:value}" + } + ${0}`, + }, + }, + }), + }, + { + name: "one dependent label multiple required attributes one required block with multiple nested required blocks with required attributes", + prefill: true, + schema: &schema.BodySchema{ + Blocks: map[string]*schema.BlockSchema{ + "resource": { + Labels: []*schema.LabelSchema{ + { + Name: "type", + IsDepKey: true, + Completable: true, + }, + { + Name: "name", + }, + }, + DependentBody: map[schema.SchemaKey]*schema.BodySchema{ + schema.NewSchemaKey(schema.DependencyKeys{ + Labels: []schema.LabelDependent{ + {Index: 0, Value: "aws_appmesh_route"}, + }, + }): { + Attributes: map[string]*schema.AttributeSchema{ + "name": { + Expr: schema.LiteralTypeOnly(cty.String), + IsRequired: true, + }, + "anothername": { + Expr: schema.LiteralTypeOnly(cty.String), + IsRequired: true, + }, + }, + Blocks: map[string]*schema.BlockSchema{ + "spec": { + Type: schema.BlockTypeList, + MinItems: 1, + Body: &schema.BodySchema{ + Attributes: map[string]*schema.AttributeSchema{ + "name": { + Expr: schema.LiteralTypeOnly(cty.String), + IsRequired: true, + }, + }, + Blocks: map[string]*schema.BlockSchema{ + "listener": { + Type: schema.BlockTypeList, + MinItems: 1, + Body: &schema.BodySchema{ + Blocks: map[string]*schema.BlockSchema{ + "port_mapping": { + Type: schema.BlockTypeList, + MinItems: 1, + Body: &schema.BodySchema{ + Attributes: map[string]*schema.AttributeSchema{ + "port": { + Expr: schema.LiteralTypeOnly(cty.Number), + IsRequired: true, + }, + "protocol": { + Expr: schema.LiteralTypeOnly(cty.String), + IsRequired: true, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + want: lang.CompleteCandidates([]lang.Candidate{ + { + Label: "aws_appmesh_route", + Kind: lang.LabelCandidateKind, + TextEdit: lang.TextEdit{ + Range: wantRange, + NewText: `aws_appmesh_route`, + Snippet: `aws_appmesh_route" "${2:name}" { + anothername = "${3:value}" + name = "${4:value}" + spec { + name = "${5:value}" + listener { + port_mapping { + port = ${6:1} + protocol = "${7:value}" + } + } + } + ${0}`, + }, + }, + }), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f, _ := hclsyntax.ParseConfig([]byte(startingConfig), "test.tf", hcl.InitialPos) + + d := NewDecoder() + d.PrefillRequiredFields = tt.prefill + d.SetSchema(tt.schema) + + err := d.LoadFile("test.tf", f) + if err != nil { + t.Fatal(err) + } + + got, err := d.CandidatesAtPos("test.tf", startingPos) + if err != nil { + t.Fatal(err) + } + + if diff := cmp.Diff(tt.want, got); diff != "" { + t.Fatalf("unexpected candidates: %s", diff) + } + }) + } +} diff --git a/schema/body_schema.go b/schema/body_schema.go index 8a79cf70..00526ddb 100644 --- a/schema/body_schema.go +++ b/schema/body_schema.go @@ -2,6 +2,7 @@ package schema import ( "fmt" + "sort" "github.com/hashicorp/go-multierror" "github.com/hashicorp/hcl-lang/lang" @@ -50,6 +51,24 @@ func NewBodySchema() *BodySchema { } } +func (as *BodySchema) AttributeNames() []string { + keys := make([]string, 0, len(as.Attributes)) + for k := range as.Attributes { + keys = append(keys, k) + } + sort.Strings(keys) + return keys +} + +func (as *BodySchema) BlockTypes() []string { + keys := make([]string, 0, len(as.Blocks)) + for k := range as.Blocks { + keys = append(keys, k) + } + sort.Strings(keys) + return keys +} + func (bs *BodySchema) Validate() error { if len(bs.Attributes) > 0 && bs.AnyAttribute != nil { return fmt.Errorf("one of Attributes or AnyAttribute must be set, not both")