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

Add support for completing labels #58

Merged
merged 1 commit into from
May 6, 2020
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
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