Skip to content

Commit

Permalink
textDocument/complete: Complete first level keywords
Browse files Browse the repository at this point in the history
  • Loading branch information
radeksimko committed May 19, 2020
1 parent 963fe38 commit 2c9b4fc
Show file tree
Hide file tree
Showing 12 changed files with 154 additions and 20 deletions.
5 changes: 5 additions & 0 deletions internal/hcl/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
6 changes: 5 additions & 1 deletion internal/terraform/lang/config_block.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ import (

type configBlockFactory interface {
New(*hclsyntax.Block) (ConfigBlock, error)
LabelSchema() LabelSchema
}


type labelCandidates map[string][]CompletionCandidate

type completableLabels struct {
Expand Down Expand Up @@ -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
}
Expand Down
7 changes: 7 additions & 0 deletions internal/terraform/lang/config_block_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package lang

import (
"fmt"
"sort"
"testing"

"github.com/google/go-cmp/cmp"
Expand Down Expand Up @@ -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
Expand Down
11 changes: 9 additions & 2 deletions internal/terraform/lang/datasource_block.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}

Expand Down
4 changes: 2 additions & 2 deletions internal/terraform/lang/labels.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
}
Expand Down
34 changes: 34 additions & 0 deletions internal/terraform/lang/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
32 changes: 32 additions & 0 deletions internal/terraform/lang/parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 7 additions & 1 deletion internal/terraform/lang/provider_block.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
Expand Down
9 changes: 8 additions & 1 deletion internal/terraform/lang/resource_block.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
Expand Down
16 changes: 16 additions & 0 deletions internal/terraform/lang/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
9 changes: 8 additions & 1 deletion internal/terraform/lang/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
type Parser interface {
SetLogger(*log.Logger)
SetSchemaReader(schema.Reader)
BlockTypeCandidates() CompletionCandidates
ParseBlockFromHCL(*hcl.Block) (ConfigBlock, error)
}

Expand All @@ -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
Expand All @@ -59,6 +65,7 @@ type Attribute struct {
// for completion loosely reflecting lsp.CompletionList
type CompletionCandidates interface {
List() []CompletionCandidate
Len() int
IsComplete() bool
}

Expand Down
33 changes: 21 additions & 12 deletions langserver/handlers/complete.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -40,35 +41,43 @@ 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)
}
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)
}

0 comments on commit 2c9b4fc

Please sign in to comment.