Skip to content

Commit

Permalink
Prefill Required Attributes
Browse files Browse the repository at this point in the history
  • Loading branch information
jpogran committed Sep 29, 2021
1 parent 4bf451b commit 0cb8396
Show file tree
Hide file tree
Showing 6 changed files with 260 additions and 43 deletions.
23 changes: 18 additions & 5 deletions decoder/block_candidates.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,16 +56,29 @@ func detailForBlock(block *schema.BlockSchema) string {

func snippetForBlock(blockType string, block *schema.BlockSchema) string {
labels := ""
placeholder := 1

depKey := false
for _, l := range block.Labels {
if l.IsDepKey {
labels += fmt.Sprintf(` "${%d}"`, placeholder)
} else {
labels += fmt.Sprintf(` "${%d:%s}"`, placeholder, l.Name)
depKey = true
}
placeholder++
}

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 \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)
}
2 changes: 1 addition & 1 deletion decoder/candidates.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}

Expand Down
2 changes: 1 addition & 1 deletion decoder/candidates_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -421,7 +421,7 @@ func TestDecoder_CandidatesAtPos_rightHandSide(t *testing.T) {
},
}
testConfig := []byte(`myblock "foo" {
num_attr =
num_attr =
}
`)

Expand Down
2 changes: 2 additions & 0 deletions decoder/decoder.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ type Decoder struct {
utmMedium string
// utm_content parameter, e.g. documentHover or documentLink
useUtmContent bool

PrefillRequiredAttributes bool
}

type ReferenceTargetReader func() lang.ReferenceTargets
Expand Down
99 changes: 63 additions & 36 deletions decoder/label_candidates.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) (lang.Candidates, error) {
candidates := lang.NewCandidates()
candidates.IsComplete = true
count := 0
Expand All @@ -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.PrefillRequiredAttributes {
snippet := generateSnippet(label.Value, bodySchema.Attributes)
te = lang.TextEdit{
NewText: snippet,
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++
}
}

Expand All @@ -81,6 +95,19 @@ func (d *Decoder) labelCandidatesFromDependentSchema(idx int, db map[schema.Sche
return candidates, nil
}

func generateSnippet(label string, attr map[string]*schema.AttributeSchema) string {
snippetText := fmt.Sprintf("%s\" \"${1:label}\" {\n", label)
placeholder := 2
for i, a := range attr {
if a.IsRequired {
snippetText += fmt.Sprintf("\t%s = \"${%d:%s}\"\n", i, placeholder, i)
placeholder++
}
}
snippetText += "\t${0}"
return snippetText
}

func sortedSchemaKeys(m map[schema.SchemaKey]*schema.BodySchema) []schema.SchemaKey {
keys := make([]schema.SchemaKey, 0)
for k := range m {
Expand Down
175 changes: 175 additions & 0 deletions decoder/label_schema_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
package decoder

import (
"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 TestCandidatesAtPos_prefillRequiredBlocks(t *testing.T) {
tests := []struct {
name string
fileName string
cfg string
position hcl.Pos
prefillRequiredAttributes bool
schema *schema.BodySchema
want lang.Candidates
}{
{
name: "simple completion",
fileName: "test.tf",
cfg: `resource "" {
}`,
position: hcl.Pos{
Line: 1,
Column: 11,
Byte: 10,
},
prefillRequiredAttributes: false,
schema: &schema.BodySchema{
Blocks: map[string]*schema.BlockSchema{
"resource": {
Labels: []*schema.LabelSchema{
{
Name: "type",
IsDepKey: true,
Completable: true,
},
},
DependentBody: map[schema.SchemaKey]*schema.BodySchema{
schema.NewSchemaKey(schema.DependencyKeys{
Labels: []schema.LabelDependent{
{Index: 0, Value: "aws_instance"},
},
}): {
Attributes: map[string]*schema.AttributeSchema{
"ami": {
Expr: schema.LiteralTypeOnly(cty.String),
IsRequired: true,
},
"instance_type": {Expr: schema.LiteralTypeOnly(cty.String)},
},
},
},
},
},
},
want: lang.CompleteCandidates([]lang.Candidate{
{
Label: "aws_instance",
TextEdit: lang.TextEdit{
Range: hcl.Range{
Filename: "test.tf",
Start: hcl.Pos{
Line: 1,
Column: 11,
Byte: 10,
},
End: hcl.Pos{
Line: 1,
Column: 11,
Byte: 10,
},
},
NewText: "aws_instance",
Snippet: "aws_instance",
},
Kind: lang.LabelCandidateKind,
},
}),
},
{
name: "prefill required attributes",
fileName: "test.tf",
cfg: `resource "" {
}`,
position: hcl.Pos{
Line: 1,
Column: 11,
Byte: 10,
},
prefillRequiredAttributes: true,
schema: &schema.BodySchema{
Blocks: map[string]*schema.BlockSchema{
"resource": {
Labels: []*schema.LabelSchema{
{
Name: "type",
IsDepKey: true,
Completable: true,
},
},
DependentBody: map[schema.SchemaKey]*schema.BodySchema{
schema.NewSchemaKey(schema.DependencyKeys{
Labels: []schema.LabelDependent{
{Index: 0, Value: "aws_instance"},
},
}): {
Attributes: map[string]*schema.AttributeSchema{
"ami": {
Expr: schema.LiteralTypeOnly(cty.String),
IsRequired: true,
},
"instance_type": {Expr: schema.LiteralTypeOnly(cty.String)},
},
},
},
},
},
},
want: lang.CompleteCandidates([]lang.Candidate{
{
Label: "aws_instance",
TextEdit: lang.TextEdit{
Range: hcl.Range{
Filename: "test.tf",
Start: hcl.Pos{
Line: 1,
Column: 11,
Byte: 10,
},
End: hcl.Pos{
Line: 1,
Column: 14,
Byte: 13,
},
},
NewText: "aws_instance\" \"${1:label}\" {\n\tami = \"${2:ami}\"\n\t${0}",
Snippet: "aws_instance\" \"${1:label}\" {\n\tami = \"${2:ami}\"\n\t${0}",
},
Kind: lang.LabelCandidateKind,
},
}),
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
f, _ := hclsyntax.ParseConfig([]byte(tt.cfg), tt.fileName, hcl.InitialPos)

d := NewDecoder()
d.PrefillRequiredAttributes = tt.prefillRequiredAttributes
d.SetSchema(tt.schema)

err := d.LoadFile(tt.fileName, f)
if err != nil {
t.Fatal(err)
}

got, err := d.CandidatesAtPos(tt.fileName, tt.position)
if err != nil {
t.Fatal(err)
}

if diff := cmp.Diff(tt.want, got); diff != "" {
t.Fatalf("unexpected candidates: %s", diff)
}
})
}
}

0 comments on commit 0cb8396

Please sign in to comment.