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

textDocument/complete: Complete first level keywords #104

Merged
merged 3 commits into from
May 21, 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
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
}
15 changes: 13 additions & 2 deletions internal/hcl/hcl.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,13 @@ func (f *file) blockAtPosition(pos hcllib.Pos) (*hcllib.Block, error) {
if body.SrcRange.Empty() && pos != hcllib.InitialPos {
return nil, &InvalidHclPosErr{pos, body.SrcRange}
}
if !body.SrcRange.Empty() && !body.SrcRange.ContainsPos(pos) {
return nil, &InvalidHclPosErr{pos, body.SrcRange}
if !body.SrcRange.Empty() {
if posIsEqual(body.SrcRange.End, pos) {
return nil, &NoBlockFoundErr{pos}
}
if !body.SrcRange.ContainsPos(pos) {
return nil, &InvalidHclPosErr{pos, body.SrcRange}
}
}
}

Expand All @@ -64,3 +69,9 @@ func (f *file) blockAtPosition(pos hcllib.Pos) (*hcllib.Block, error) {

return block, nil
}

func posIsEqual(a, b hcllib.Pos) bool {
return a.Byte == b.Byte &&
a.Column == b.Column &&
a.Line == b.Line
}
14 changes: 14 additions & 0 deletions internal/hcl/hcl_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,20 @@ func TestFile_BlockAtPosition(t *testing.T) {
},
nil,
},
{
"valid config and EOF position",
`provider "aws" {

}
`,
hcl.Pos{
Line: 4,
Column: 1,
Byte: 20,
},
&NoBlockFoundErr{AtPos: hcl.Pos{Line: 4, Column: 1, Byte: 20}},
nil,
},
}

opts := cmp.Options{
Expand Down
19 changes: 16 additions & 3 deletions internal/source/source.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,18 +25,31 @@ func (l sourceLine) Bytes() []byte {

func MakeSourceLines(filename string, s []byte) []Line {
var ret []Line
if len(s) == 0 {
return ret
}

lastRng := hcl.Range{
Filename: filename,
Start: hcl.InitialPos,
End: hcl.InitialPos,
}
sc := hcl.NewRangeScanner(s, filename, scanLines)
for sc.Scan() {
ret = append(ret, sourceLine{
content: sc.Bytes(),
rng: sc.Range(),
})
lastRng = sc.Range()
}

// Account for the last (virtual) user-percieved line
ret = append(ret, sourceLine{
content: []byte{},
rng: hcl.Range{
Filename: lastRng.Filename,
Start: lastRng.End,
End: lastRng.End,
},
})

return ret
}

Expand Down
6 changes: 3 additions & 3 deletions internal/source/source_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,15 @@ import (

func TestMakeSourceLines_empty(t *testing.T) {
lines := MakeSourceLines("/test.tf", []byte{})
if len(lines) != 0 {
t.Fatalf("Expected no lines from empty file, %d parsed:\n%#v",
if len(lines) != 1 {
t.Fatalf("Expected 1 line from empty file, %d parsed:\n%#v",
len(lines), lines)
}
}

func TestMakeSourceLines_success(t *testing.T) {
lines := MakeSourceLines("/test.tf", []byte("\n\n\n\n"))
expectedLines := 4
expectedLines := 5
if len(lines) != expectedLines {
t.Fatalf("Expected exactly %d lines, %d parsed",
expectedLines, len(lines))
Expand Down
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
9 changes: 8 additions & 1 deletion 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 @@ -250,7 +251,7 @@ func TestCompletableBlock_CompletionCandidatesAtPos(t *testing.T) {

cb := &completableBlock{
logger: testLogger(),
block: ParseBlock(block, []*Label{}, tc.sb),
block: ParseBlock(block, []*ParsedLabel{}, tc.sb),
}

list, err := cb.completionCandidatesAtPos(tc.pos)
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
15 changes: 11 additions & 4 deletions internal/terraform/lang/datasource_block.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,21 +24,28 @@ 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"
}

type datasourceBlock struct {
logger *log.Logger

labelSchema LabelSchema
labels []*Label
labels []*ParsedLabel
hclBlock *hclsyntax.Block
sr schema.Reader
}
Expand All @@ -64,7 +71,7 @@ func (r *datasourceBlock) Name() string {
return fmt.Sprintf("%s.%s", firstLabel, secondLabel)
}

func (r *datasourceBlock) Labels() []*Label {
func (r *datasourceBlock) Labels() []*ParsedLabel {
if r.labels != nil {
return r.labels
}
Expand Down
4 changes: 2 additions & 2 deletions internal/terraform/lang/hcl_block.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import (

type parsedBlock struct {
hclBlock *hclsyntax.Block
labels []*Label
labels []*ParsedLabel
AttributesMap map[string]*Attribute
BlockTypesMap map[string]*BlockType

Expand Down Expand Up @@ -59,7 +59,7 @@ func (b *parsedBlock) PosInLabels(pos hcl.Pos) bool {
return false
}

func (b *parsedBlock) LabelAtPos(pos hcl.Pos) (*Label, bool) {
func (b *parsedBlock) LabelAtPos(pos hcl.Pos) (*ParsedLabel, 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
Expand Down
2 changes: 1 addition & 1 deletion internal/terraform/lang/hcl_block_type.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ func (bt BlockTypes) AddBlock(name string, block *hclsyntax.Block, typeSchema *t

if block != nil {
// SDK doesn't support named blocks yet, so we expect no labels here for now
labels := make([]*Label, 0)
labels := make([]*ParsedLabel, 0)
bt[name].BlockList = append(bt[name].BlockList, ParseBlock(block, labels, typeSchema.Block))
}
}
Expand Down
2 changes: 1 addition & 1 deletion internal/terraform/lang/hcl_parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (

// ParseBlock parses HCL configuration based on tfjson's SchemaBlock
// and keeps hold of all tfjson schema details on block or attribute level
func ParseBlock(block *hclsyntax.Block, labels []*Label, schema *tfjson.SchemaBlock) Block {
func ParseBlock(block *hclsyntax.Block, labels []*ParsedLabel, schema *tfjson.SchemaBlock) Block {
b := &parsedBlock{
hclBlock: block,
labels: labels,
Expand Down
8 changes: 4 additions & 4 deletions internal/terraform/lang/hcl_parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -307,7 +307,7 @@ func TestParseBlock_attributesAndBlockTypes(t *testing.T) {
t.Fatal(err)
}

b := ParseBlock(block, []*Label{}, tc.schema)
b := ParseBlock(block, []*ParsedLabel{}, tc.schema)

if diff := cmp.Diff(tc.expectedAttributes, b.Attributes(), opts...); diff != "" {
t.Fatalf("Attributes don't match.\n%s", diff)
Expand Down Expand Up @@ -471,7 +471,7 @@ func TestBlock_BlockAtPos(t *testing.T) {
t.Fatal(err)
}

b := ParseBlock(block, []*Label{}, schema)
b := ParseBlock(block, []*ParsedLabel{}, schema)
fBlock, _ := b.BlockAtPos(tc.pos)
if diff := cmp.Diff(tc.expectedBlock, fBlock, opts...); diff != "" {
t.Fatalf("Block doesn't match.\n%s", diff)
Expand Down Expand Up @@ -631,7 +631,7 @@ func TestBlock_PosInBody(t *testing.T) {
t.Fatal(err)
}

b := ParseBlock(block, []*Label{}, schema)
b := ParseBlock(block, []*ParsedLabel{}, schema)
isInBody := b.PosInBody(tc.pos)
if tc.expected != isInBody {
if tc.expected {
Expand Down Expand Up @@ -766,7 +766,7 @@ func TestBlock_PosInAttributes(t *testing.T) {
t.Fatal(err)
}

b := ParseBlock(block, []*Label{}, schema)
b := ParseBlock(block, []*ParsedLabel{}, schema)
isInAttribute := b.PosInAttribute(tc.pos)
if tc.expected != isInAttribute {
if tc.expected {
Expand Down
10 changes: 5 additions & 5 deletions internal/terraform/lang/labels.go
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
package lang

func parseLabels(blockType string, schema LabelSchema, parsed []string) []*Label {
labels := make([]*Label, len(schema))
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] = &Label{
Name: labelName,
labels[i] = &ParsedLabel{
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
Loading