Skip to content

Commit

Permalink
Add support for completing labels
Browse files Browse the repository at this point in the history
  • Loading branch information
radeksimko committed Apr 22, 2020
1 parent ea32405 commit 29a0b61
Show file tree
Hide file tree
Showing 12 changed files with 357 additions and 18 deletions.
55 changes: 52 additions & 3 deletions internal/terraform/lang/config_block.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,44 @@ type configBlockFactory interface {
New(*hclsyntax.Block) (ConfigBlock, error)
}


type labelCandidates map[string][]CompletionCandidate

type completableLabels struct {
logger *log.Logger
block Block
labels labelCandidates
}

func (cl *completableLabels) completionCandidatesAtPos(pos hcl.Pos) (CompletionCandidates, error) {
list := &completeList{
candidates: make([]CompletionCandidate, 0),
}
l, ok := cl.block.LabelAtPos(pos)
if !ok {
cl.logger.Printf("label not found at %#v", pos)
return list, nil
}
candidates, ok := cl.labels[l.Name]
if !ok {
cl.logger.Printf("label %q doesn't have completion candidates", l.Name)
return list, nil
}

cl.logger.Printf("completing label %q ...", l.Name)
for _, c := range candidates {
list.candidates = append(list.candidates, c)
}
list.Sort()

return list, nil
}

// completableBlock provides common completion functionality
// for any Block implementation
type completableBlock struct {
logger *log.Logger

block Block
block Block
}

func (cb *completableBlock) completionCandidatesAtPos(pos hcl.Pos) (CompletionCandidates, error) {
Expand All @@ -27,7 +59,6 @@ func (cb *completableBlock) completionCandidatesAtPos(pos hcl.Pos) (CompletionCa
}

if !cb.block.PosInBody(pos) {
// Avoid autocompleting outside of body, for now
cb.logger.Println("avoiding completion outside of block body")
return nil, nil
}
Expand All @@ -37,6 +68,7 @@ func (cb *completableBlock) completionCandidatesAtPos(pos hcl.Pos) (CompletionCa
return nil, nil
}

// Completing the body (attributes and nested blocks)
b, ok := cb.block.BlockAtPos(pos)
if !ok {
// This should never happen as the completion
Expand Down Expand Up @@ -92,6 +124,23 @@ func (l *completeList) IsComplete() bool {
return true
}

type labelCandidate struct {
label string
detail string
}

func (c *labelCandidate) Label() string {
return c.label
}

func (c *labelCandidate) Detail() string {
return c.detail
}

func (c *labelCandidate) Snippet(pos hcl.Pos) (hcl.Pos, string) {
return pos, c.label
}

type attributeCandidate struct {
Name string
Attr *Attribute
Expand Down
38 changes: 33 additions & 5 deletions internal/terraform/lang/datasource_block.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,10 +81,6 @@ func (r *datasourceBlock) CompletionCandidatesAtPos(pos hcl.Pos) (CompletionCand
return nil, &noSchemaReaderErr{r.BlockType()}
}

cb := &completableBlock{
logger: r.logger,
}

var schemaBlock *tfjson.SchemaBlock
if r.Type() != "" {
rSchema, err := r.sr.DataSourceSchema(r.Type())
Expand All @@ -93,7 +89,39 @@ func (r *datasourceBlock) CompletionCandidatesAtPos(pos hcl.Pos) (CompletionCand
}
schemaBlock = rSchema.Block
}
cb.block = ParseBlock(r.hclBlock, r.Labels(), schemaBlock)
block := ParseBlock(r.hclBlock, r.Labels(), schemaBlock)

if block.PosInLabels(pos) {
dataSources, err := r.sr.DataSources()
if err != nil {
return nil, err
}

cl := &completableLabels{
logger: r.logger,
block: block,
labels: labelCandidates{
"type": dataSourceCandidates(dataSources),
},
}

return cl.completionCandidatesAtPos(pos)
}

cb := &completableBlock{
logger: r.logger,
block: block,
}
return cb.completionCandidatesAtPos(pos)
}

func dataSourceCandidates(dataSources []schema.DataSource) []CompletionCandidate {
candidates := []CompletionCandidate{}
for _, ds := range dataSources {
candidates = append(candidates, &labelCandidate{
label: ds.Name,
detail: ds.Provider,
})
}
return candidates
}
19 changes: 19 additions & 0 deletions internal/terraform/lang/datasource_block_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,25 @@ func TestDataSourceBlock_completionCandidatesAtPos(t *testing.T) {
[]renderedCandidate{},
errors.New("error getting schema"),
},
{
"datasource names",
`data "" "" {
}`,
simpleSchema,
nil,
hcl.Pos{Line: 1, Column: 5, Byte: 6},
[]renderedCandidate{
{
Label: "custom_ds",
Detail: "custom",
Snippet: renderedSnippet{
Pos: hcl.Pos{Line: 1, Column: 5, Byte: 6},
Text: "custom_ds",
},
},
},
nil,
},
}

for i, tc := range testCases {
Expand Down
21 changes: 21 additions & 0 deletions internal/terraform/lang/hcl_block.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,27 @@ func (b *parsedBlock) Range() hcl.Range {
return b.hclBlock.Range()
}

func (b *parsedBlock) PosInLabels(pos hcl.Pos) bool {
for _, rng := range b.hclBlock.LabelRanges {
if rng.ContainsPos(pos) {
return true
}
}

return false
}

func (b *parsedBlock) LabelAtPos(pos hcl.Pos) (*Label, bool) {
for i, rng := range b.hclBlock.LabelRanges {
if rng.ContainsPos(pos) {
// TODO: Guard against crashes when user sets label where we don't expect it
return b.labels[i], true
}
}

return nil, false
}

func (b *parsedBlock) PosInBody(pos hcl.Pos) bool {
for _, blockType := range b.BlockTypesMap {
for _, b := range blockType.BlockList {
Expand Down
38 changes: 33 additions & 5 deletions internal/terraform/lang/provider_block.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,6 @@ func (p *providerBlock) CompletionCandidatesAtPos(pos hcl.Pos) (CompletionCandid
return nil, &noSchemaReaderErr{p.BlockType()}
}

cb := &completableBlock{
logger: p.logger,
}

var schemaBlock *tfjson.SchemaBlock
if p.RawName() != "" {
pSchema, err := p.sr.ProviderConfigSchema(p.RawName())
Expand All @@ -83,7 +79,39 @@ func (p *providerBlock) CompletionCandidatesAtPos(pos hcl.Pos) (CompletionCandid
}
schemaBlock = pSchema.Block
}
cb.block = ParseBlock(p.hclBlock, p.Labels(), schemaBlock)
block := ParseBlock(p.hclBlock, p.Labels(), schemaBlock)

if block.PosInLabels(pos) {
providers, err := p.sr.Providers()
if err != nil {
return nil, err
}

cl := &completableLabels{
logger: p.logger,
block: block,
labels: labelCandidates{
"name": providerCandidates(providers),
},
}

return cl.completionCandidatesAtPos(pos)
}

cb := &completableBlock{
logger: p.logger,
block: block,
}
return cb.completionCandidatesAtPos(pos)
}

func providerCandidates(names []string) []CompletionCandidate {
candidates := []CompletionCandidate{}
for _, name := range names {
candidates = append(candidates, &labelCandidate{
label: name,
detail: "provider",
})
}
return candidates
}
20 changes: 20 additions & 0 deletions internal/terraform/lang/provider_block_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,26 @@ func TestProviderBlock_completionCandidatesAtPos(t *testing.T) {
[]renderedCandidate{},
errors.New("error getting schema"),
},
{
"provider names",
`provider "" {
}`,
simpleSchema,
nil,
hcl.Pos{Line: 1, Column: 9, Byte: 10},
[]renderedCandidate{
{
Label: "custom",
Detail: "provider",
Snippet: renderedSnippet{
Pos: hcl.Pos{Line: 1, Column: 9, Byte: 10},
Text: "custom",
},
},
},
nil,
},
}

for i, tc := range testCases {
Expand Down
37 changes: 32 additions & 5 deletions internal/terraform/lang/resource_block.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,10 +81,6 @@ func (r *resourceBlock) CompletionCandidatesAtPos(pos hcl.Pos) (CompletionCandid
return nil, &noSchemaReaderErr{r.BlockType()}
}

cb := &completableBlock{
logger: r.logger,
}

var schemaBlock *tfjson.SchemaBlock
if r.Type() != "" {
rSchema, err := r.sr.ResourceSchema(r.Type())
Expand All @@ -93,7 +89,38 @@ func (r *resourceBlock) CompletionCandidatesAtPos(pos hcl.Pos) (CompletionCandid
}
schemaBlock = rSchema.Block
}
cb.block = ParseBlock(r.hclBlock, r.Labels(), schemaBlock)
block := ParseBlock(r.hclBlock, r.Labels(), schemaBlock)

if block.PosInLabels(pos) {
resources, err := r.sr.Resources()
if err != nil {
return nil, err
}
cl := &completableLabels{
logger: r.logger,
block: block,
labels: labelCandidates{
"type": resourceCandidates(resources),
},
}

return cl.completionCandidatesAtPos(pos)
}

cb := &completableBlock{
logger: r.logger,
block: block,
}
return cb.completionCandidatesAtPos(pos)
}

func resourceCandidates(resources []schema.Resource) []CompletionCandidate {
candidates := []CompletionCandidate{}
for _, r := range resources {
candidates = append(candidates, &labelCandidate{
label: r.Name,
detail: r.Provider,
})
}
return candidates
}
20 changes: 20 additions & 0 deletions internal/terraform/lang/resource_block_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,26 @@ func TestResourceBlock_completionCandidatesAtPos(t *testing.T) {
[]renderedCandidate{},
errors.New("error getting schema"),
},
{
"resource names",
`resource "" "" {
}`,
simpleSchema,
nil,
hcl.Pos{Line: 1, Column: 9, Byte: 10},
[]renderedCandidate{
{
Label: "custom_rs",
Detail: "custom",
Snippet: renderedSnippet{
Pos: hcl.Pos{Line: 1, Column: 9, Byte: 10},
Text: "custom_rs",
},
},
},
nil,
},
}

for i, tc := range testCases {
Expand Down
2 changes: 2 additions & 0 deletions internal/terraform/lang/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@ type ConfigBlock interface {
// which keeps track of the related schema
type Block interface {
BlockAtPos(pos hcl.Pos) (Block, bool)
LabelAtPos(pos hcl.Pos) (*Label, bool)
Range() hcl.Range
PosInLabels(pos hcl.Pos) bool
PosInBody(pos hcl.Pos) bool
PosInAttribute(pos hcl.Pos) bool
Attributes() map[string]*Attribute
Expand Down
Loading

0 comments on commit 29a0b61

Please sign in to comment.