Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Prefill Required Fields on Completion #89

Merged
merged 1 commit into from
Oct 7, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 40 additions & 3 deletions decoder/block_candidates.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 {
Expand All @@ -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

Expand Down
3 changes: 2 additions & 1 deletion decoder/body_candidates.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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++
}

Expand Down
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, bSchema.Labels)
}
}

Expand Down
3 changes: 3 additions & 0 deletions decoder/decoder.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
53 changes: 53 additions & 0 deletions decoder/expression_candidates.go
Original file line number Diff line number Diff line change
Expand Up @@ -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]"
radeksimko marked this conversation as resolved.
Show resolved Hide resolved
}
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
}
Expand Down
195 changes: 159 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, labelSchemas []*schema.LabelSchema) (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.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++
}
}

Expand All @@ -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 {
Expand Down
Loading