diff --git a/internal/hcl/errors.go b/internal/hcl/errors.go index 49e8e333b..2bfeda796 100644 --- a/internal/hcl/errors.go +++ b/internal/hcl/errors.go @@ -22,3 +22,8 @@ type NoBlockFoundErr struct { func (e *NoBlockFoundErr) Error() string { return fmt.Sprintf("no block found at %#v", e.AtPos) } + +func IsNoBlockFoundErr(err error) bool { + _, ok := err.(*NoBlockFoundErr) + return ok +} diff --git a/internal/terraform/lang/config_block.go b/internal/terraform/lang/config_block.go index 402af85f5..5dd70ae00 100644 --- a/internal/terraform/lang/config_block.go +++ b/internal/terraform/lang/config_block.go @@ -11,9 +11,9 @@ import ( type configBlockFactory interface { New(*hclsyntax.Block) (ConfigBlock, error) + LabelSchema() LabelSchema } - type labelCandidates map[string][]CompletionCandidate type completableLabels struct { @@ -120,6 +120,10 @@ func (l *completeList) List() []CompletionCandidate { return l.candidates } +func (l *completeList) Len() int { + return len(l.candidates) +} + func (l *completeList) IsComplete() bool { return true } diff --git a/internal/terraform/lang/config_block_test.go b/internal/terraform/lang/config_block_test.go index 50b9d7b5c..a935d224d 100644 --- a/internal/terraform/lang/config_block_test.go +++ b/internal/terraform/lang/config_block_test.go @@ -2,6 +2,7 @@ package lang import ( "fmt" + "sort" "testing" "github.com/google/go-cmp/cmp" @@ -294,6 +295,12 @@ func renderCandidates(list CompletionCandidates, pos hcl.Pos) []renderedCandidat return rendered } +func sortRenderedCandidates(candidates []renderedCandidate) { + sort.Slice(candidates, func(i, j int) bool { + return candidates[i].Label < candidates[j].Label + }) +} + type renderedCandidate struct { Label string Detail string diff --git a/internal/terraform/lang/datasource_block.go b/internal/terraform/lang/datasource_block.go index dd52c595e..5bb023a33 100644 --- a/internal/terraform/lang/datasource_block.go +++ b/internal/terraform/lang/datasource_block.go @@ -24,13 +24,20 @@ func (f *datasourceBlockFactory) New(block *hclsyntax.Block) (ConfigBlock, error return &datasourceBlock{ logger: f.logger, - labelSchema: LabelSchema{"type", "name"}, + labelSchema: f.LabelSchema(), hclBlock: block, sr: f.schemaReader, }, nil } -func (r *datasourceBlockFactory) BlockType() string { +func (f *datasourceBlockFactory) LabelSchema() LabelSchema { + return LabelSchema{ + Label{Name: "type", IsCompletable: true}, + Label{Name: "name"}, + } +} + +func (f *datasourceBlockFactory) BlockType() string { return "data" } diff --git a/internal/terraform/lang/labels.go b/internal/terraform/lang/labels.go index 0277840d9..31e5ccc2b 100644 --- a/internal/terraform/lang/labels.go +++ b/internal/terraform/lang/labels.go @@ -3,13 +3,13 @@ package lang func parseLabels(blockType string, schema LabelSchema, parsed []string) []*ParsedLabel { labels := make([]*ParsedLabel, len(schema)) - for i, labelName := range schema { + for i, l := range schema { var value string if len(parsed)-1 >= i { value = parsed[i] } labels[i] = &ParsedLabel{ - Name: labelName, + Name: l.Name, Value: value, } } diff --git a/internal/terraform/lang/parser.go b/internal/terraform/lang/parser.go index 2eb6e73cf..88a13e0ba 100644 --- a/internal/terraform/lang/parser.go +++ b/internal/terraform/lang/parser.go @@ -91,6 +91,40 @@ func (p *parser) blockTypes() map[string]configBlockFactory { } } +func (p *parser) BlockTypeCandidates() CompletionCandidates { + bTypes := p.blockTypes() + + list := &completeList{ + candidates: make([]CompletionCandidate, 0), + } + + for name, t := range bTypes { + list.candidates = append(list.candidates, &completableBlockType{ + TypeName: name, + LabelSchema: t.LabelSchema(), + }) + } + + return list +} + +type completableBlockType struct { + TypeName string + LabelSchema LabelSchema +} + +func (bt *completableBlockType) Label() string { + return bt.TypeName +} + +func (bt *completableBlockType) Snippet(pos hcl.Pos) (hcl.Pos, string) { + return pos, snippetForBlock(bt.TypeName, bt.LabelSchema) +} + +func (bt *completableBlockType) Detail() string { + return "" +} + func (p *parser) ParseBlockFromHCL(block *hcl.Block) (ConfigBlock, error) { if block == nil { return nil, EmptyConfigErr diff --git a/internal/terraform/lang/parser_test.go b/internal/terraform/lang/parser_test.go index 29edcc4d1..968e0cb47 100644 --- a/internal/terraform/lang/parser_test.go +++ b/internal/terraform/lang/parser_test.go @@ -8,10 +8,42 @@ import ( "os" "testing" + "github.com/google/go-cmp/cmp" "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hclsyntax" ) +func TestParser_BlockTypeCandidates_len(t *testing.T) { + p := newParser() + + candidates := p.BlockTypeCandidates() + if candidates.Len() < 3 { + t.Fatalf("Expected >= 3 candidates, %d given", candidates.Len()) + } +} + +func TestParser_BlockTypeCandidates_snippet(t *testing.T) { + p := newParser() + + list := p.BlockTypeCandidates() + rendered := renderCandidates(list, hcl.InitialPos) + sortRenderedCandidates(rendered) + + expectedCandidate := renderedCandidate{ + Label: "data", + Detail: "", + Snippet: renderedSnippet{ + Pos: hcl.InitialPos, + Text: `data "${1}" "${2:name}" { + ${3} +}`, + }, + } + if diff := cmp.Diff(expectedCandidate, rendered[0]); diff != "" { + t.Fatalf("Completion candidate does not match.\n%s", diff) + } +} + func TestParser_ParseBlockFromHCL(t *testing.T) { testCases := []struct { name string diff --git a/internal/terraform/lang/provider_block.go b/internal/terraform/lang/provider_block.go index 33a788830..338b04b43 100644 --- a/internal/terraform/lang/provider_block.go +++ b/internal/terraform/lang/provider_block.go @@ -23,12 +23,18 @@ func (f *providerBlockFactory) New(block *hclsyntax.Block) (ConfigBlock, error) return &providerBlock{ logger: f.logger, - labelSchema: LabelSchema{"name"}, + labelSchema: f.LabelSchema(), hclBlock: block, sr: f.schemaReader, }, nil } +func (f *providerBlockFactory) LabelSchema() LabelSchema { + return LabelSchema{ + Label{Name: "name", IsCompletable: true}, + } +} + func (f *providerBlockFactory) BlockType() string { return "provider" } diff --git a/internal/terraform/lang/resource_block.go b/internal/terraform/lang/resource_block.go index dbbabcdf5..425958319 100644 --- a/internal/terraform/lang/resource_block.go +++ b/internal/terraform/lang/resource_block.go @@ -24,12 +24,19 @@ func (f *resourceBlockFactory) New(block *hclsyntax.Block) (ConfigBlock, error) return &resourceBlock{ logger: f.logger, - labelSchema: LabelSchema{"type", "name"}, + labelSchema: f.LabelSchema(), hclBlock: block, sr: f.schemaReader, }, nil } +func (f *resourceBlockFactory) LabelSchema() LabelSchema { + return LabelSchema{ + Label{Name: "type", IsCompletable: true}, + Label{Name: "name", IsCompletable: false}, + } +} + func (r *resourceBlockFactory) BlockType() string { return "resource" } diff --git a/internal/terraform/lang/schema.go b/internal/terraform/lang/schema.go index 323845fda..af2e442af 100644 --- a/internal/terraform/lang/schema.go +++ b/internal/terraform/lang/schema.go @@ -44,6 +44,22 @@ func snippetForNestedBlock(name string) string { return fmt.Sprintf("%s {\n ${0}\n}", name) } +func snippetForBlock(name string, labelSchema LabelSchema) string { + bodyPlaceholder := 0 + labels := make([]string, len(labelSchema)) + for i, l := range labelSchema { + if l.IsCompletable { + labels[i] = fmt.Sprintf(`"${%d}"`, i+1) + } else { + labels[i] = fmt.Sprintf(`"${%d:%s}"`, i+1, l.Name) + } + bodyPlaceholder = i + 2 + } + + return fmt.Sprintf("%s %s {\n ${%d}\n}", + name, strings.Join(labels, " "), bodyPlaceholder) +} + func schemaAttributeDetail(attr *tfjson.SchemaAttribute) string { var requiredText string if attr.Optional { diff --git a/internal/terraform/lang/types.go b/internal/terraform/lang/types.go index 59e26f7fe..b34fbc7f1 100644 --- a/internal/terraform/lang/types.go +++ b/internal/terraform/lang/types.go @@ -13,6 +13,7 @@ import ( type Parser interface { SetLogger(*log.Logger) SetSchemaReader(schema.Reader) + BlockTypeCandidates() CompletionCandidates ParseBlockFromHCL(*hcl.Block) (ConfigBlock, error) } @@ -38,7 +39,12 @@ type Block interface { BlockTypes() map[string]*BlockType } -type LabelSchema []string +type LabelSchema []Label + +type Label struct { + Name string + IsCompletable bool +} type ParsedLabel struct { Name string @@ -59,6 +65,7 @@ type Attribute struct { // for completion loosely reflecting lsp.CompletionList type CompletionCandidates interface { List() []CompletionCandidate + Len() int IsComplete() bool } diff --git a/langserver/handlers/complete.go b/langserver/handlers/complete.go index 8946f3602..d90815440 100644 --- a/langserver/handlers/complete.go +++ b/langserver/handlers/complete.go @@ -4,8 +4,9 @@ import ( "context" "fmt" + hcl "github.com/hashicorp/hcl/v2" lsctx "github.com/hashicorp/terraform-ls/internal/context" - "github.com/hashicorp/terraform-ls/internal/hcl" + ihcl "github.com/hashicorp/terraform-ls/internal/hcl" ilsp "github.com/hashicorp/terraform-ls/internal/lsp" "github.com/hashicorp/terraform-ls/internal/terraform/lang" lsp "github.com/sourcegraph/go-lsp" @@ -40,18 +41,12 @@ func (h *logHandler) TextDocumentComplete(ctx context.Context, params lsp.Comple if err != nil { return list, err } - hclFile := hcl.NewFile(file) + hclFile := ihcl.NewFile(file) fPos, err := ilsp.FilePositionFromDocumentPosition(params.TextDocumentPositionParams, file) if err != nil { return list, err } - hclBlock, hclPos, err := hclFile.BlockAtPosition(fPos) - if err != nil { - return list, fmt.Errorf("finding HCL block failed: %s", err) - } - h.logger.Printf("HCL block found at HCL pos %#v", hclPos) - p, err := lang.FindCompatibleParser(tfVersion) if err != nil { return list, fmt.Errorf("finding compatible parser failed: %w", err) @@ -59,16 +54,30 @@ func (h *logHandler) TextDocumentComplete(ctx context.Context, params lsp.Comple p.SetLogger(h.logger) p.SetSchemaReader(sr) - cfgBlock, err := p.ParseBlockFromHCL(hclBlock) + hclBlock, hclPos, err := hclFile.BlockAtPosition(fPos) if err != nil { - return list, fmt.Errorf("finding config block failed: %w", err) + if ihcl.IsNoBlockFoundErr(err) { + return ilsp.CompletionList(p.BlockTypeCandidates(), fPos.Position(), cc.TextDocument), nil + } + + return list, fmt.Errorf("finding HCL block failed: %#v", err) } - h.logger.Printf("Configuration block %q parsed", cfgBlock.BlockType()) - candidates, err := cfgBlock.CompletionCandidatesAtPos(hclPos) + h.logger.Printf("HCL block found at HCL pos %#v", hclPos) + candidates, err := h.completeBlock(p, hclBlock, hclPos) if err != nil { return list, fmt.Errorf("finding completion items failed: %w", err) } return ilsp.CompletionList(candidates, fPos.Position(), cc.TextDocument), nil } + +func (h *logHandler) completeBlock(p lang.Parser, block *hcl.Block, pos hcl.Pos) (lang.CompletionCandidates, error) { + cfgBlock, err := p.ParseBlockFromHCL(block) + if err != nil { + return nil, fmt.Errorf("finding config block failed: %w", err) + } + h.logger.Printf("Configuration block %q parsed", cfgBlock.BlockType()) + + return cfgBlock.CompletionCandidatesAtPos(pos) +}