Skip to content

Commit

Permalink
Improve traversal completion for lists, tuples, and sets (#344)
Browse files Browse the repository at this point in the history
* decoder: Improve traversal completion for list expr

* decoder: Improve traversal completion for set expr

* Remove OneOf from test cases

* decoder: Improve traversal completion for tuple expr
  • Loading branch information
dbanck authored Nov 14, 2023
1 parent 0386492 commit 0c7d286
Show file tree
Hide file tree
Showing 6 changed files with 278 additions and 0 deletions.
8 changes: 8 additions & 0 deletions decoder/expr_list_completion.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,14 @@ func (list List) CompletionAtPos(ctx context.Context, pos hcl.Pos) []lang.Candid
if elemExpr.Range().ContainsPos(pos) || elemExpr.Range().End.Byte == pos.Byte {
return newExpression(list.pathCtx, elemExpr, list.cons.Elem).CompletionAtPos(ctx, pos)
}
if pos.Byte-elemExpr.Range().End.Byte == 1 {
fileBytes := list.pathCtx.Files[eType.Range().Filename].Bytes
trailingRune := fileBytes[elemExpr.Range().End.Byte:pos.Byte][0]

if trailingRune == '.' {
return newExpression(list.pathCtx, elemExpr, list.cons.Elem).CompletionAtPos(ctx, pos)
}
}
}

expr := newEmptyExpressionAtPos(eType.Range().Filename, pos)
Expand Down
84 changes: 84 additions & 0 deletions decoder/expr_list_completion_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (

"github.com/google/go-cmp/cmp"
"github.com/hashicorp/hcl-lang/lang"
"github.com/hashicorp/hcl-lang/reference"
"github.com/hashicorp/hcl-lang/schema"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
Expand Down Expand Up @@ -687,3 +688,86 @@ func TestCompletionAtPos_exprList(t *testing.T) {
})
}
}

func TestCompletionAtPos_exprList_references(t *testing.T) {
testCases := []struct {
testName string
attrSchema map[string]*schema.AttributeSchema
refTargets reference.Targets
cfg string
pos hcl.Pos
expectedCandidates lang.Candidates
}{
{
"single-line with trailing dot",
map[string]*schema.AttributeSchema{
"attr": {
Constraint: schema.List{
Elem: schema.Reference{OfScopeId: lang.ScopeId("variable")},
},
},
},
reference.Targets{
{
Addr: lang.Address{
lang.RootStep{Name: "var"},
lang.AttrStep{Name: "bar"},
},
RangePtr: &hcl.Range{
Filename: "variables.tf",
Start: hcl.Pos{Line: 2, Column: 1, Byte: 17},
End: hcl.Pos{Line: 2, Column: 3, Byte: 19},
},
ScopeId: lang.ScopeId("variable"),
},
},
`attr = [ var. ]
`,
hcl.Pos{Line: 1, Column: 14, Byte: 13},
lang.CompleteCandidates([]lang.Candidate{
{
Label: "var.bar",
Detail: "reference",
Kind: lang.TraversalCandidateKind,
TextEdit: lang.TextEdit{
NewText: "var.bar",
Snippet: "var.bar",
Range: hcl.Range{
Filename: "test.tf",
Start: hcl.Pos{Line: 1, Column: 10, Byte: 9},
End: hcl.Pos{Line: 1, Column: 14, Byte: 13},
},
},
},
}),
},
}

for i, tc := range testCases {
t.Run(fmt.Sprintf("%2d-%s", i, tc.testName), func(t *testing.T) {
bodySchema := &schema.BodySchema{
Attributes: tc.attrSchema,
}

f, _ := hclsyntax.ParseConfig([]byte(tc.cfg), "test.tf", hcl.InitialPos)
d := testPathDecoder(t, &PathContext{
Schema: bodySchema,
Files: map[string]*hcl.File{
"test.tf": f,
},
ReferenceTargets: tc.refTargets,
})

ctx := context.Background()
candidates, err := d.CandidatesAtPos(ctx, "test.tf", tc.pos)
if err != nil {
t.Fatal(err)
}

if diff := cmp.Diff(tc.expectedCandidates, candidates); diff != "" {
t.Logf("position: %#v in config: %s", tc.pos, tc.cfg)
t.Fatalf("unexpected candidates: %s", diff)
}
})
}
}
8 changes: 8 additions & 0 deletions decoder/expr_set_completion.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,14 @@ func (set Set) CompletionAtPos(ctx context.Context, pos hcl.Pos) []lang.Candidat
if elemExpr.Range().ContainsPos(pos) || elemExpr.Range().End.Byte == pos.Byte {
return newExpression(set.pathCtx, elemExpr, set.cons.Elem).CompletionAtPos(ctx, pos)
}
if pos.Byte-elemExpr.Range().End.Byte == 1 {
fileBytes := set.pathCtx.Files[eType.Range().Filename].Bytes
trailingRune := fileBytes[elemExpr.Range().End.Byte:pos.Byte][0]

if trailingRune == '.' {
return newExpression(set.pathCtx, elemExpr, set.cons.Elem).CompletionAtPos(ctx, pos)
}
}
}

expr := newEmptyExpressionAtPos(eType.Range().Filename, pos)
Expand Down
84 changes: 84 additions & 0 deletions decoder/expr_set_completion_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (

"github.com/google/go-cmp/cmp"
"github.com/hashicorp/hcl-lang/lang"
"github.com/hashicorp/hcl-lang/reference"
"github.com/hashicorp/hcl-lang/schema"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
Expand Down Expand Up @@ -648,3 +649,86 @@ func TestCompletionAtPos_exprSet(t *testing.T) {
})
}
}

func TestCompletionAtPos_exprSet_references(t *testing.T) {
testCases := []struct {
testName string
attrSchema map[string]*schema.AttributeSchema
refTargets reference.Targets
cfg string
pos hcl.Pos
expectedCandidates lang.Candidates
}{
{
"single-line with trailing dot",
map[string]*schema.AttributeSchema{
"attr": {
Constraint: schema.Set{
Elem: schema.Reference{OfScopeId: lang.ScopeId("variable")},
},
},
},
reference.Targets{
{
Addr: lang.Address{
lang.RootStep{Name: "var"},
lang.AttrStep{Name: "bar"},
},
RangePtr: &hcl.Range{
Filename: "variables.tf",
Start: hcl.Pos{Line: 2, Column: 1, Byte: 17},
End: hcl.Pos{Line: 2, Column: 3, Byte: 19},
},
ScopeId: lang.ScopeId("variable"),
},
},
`attr = [ var. ]
`,
hcl.Pos{Line: 1, Column: 14, Byte: 13},
lang.CompleteCandidates([]lang.Candidate{
{
Label: "var.bar",
Detail: "reference",
Kind: lang.TraversalCandidateKind,
TextEdit: lang.TextEdit{
NewText: "var.bar",
Snippet: "var.bar",
Range: hcl.Range{
Filename: "test.tf",
Start: hcl.Pos{Line: 1, Column: 10, Byte: 9},
End: hcl.Pos{Line: 1, Column: 14, Byte: 13},
},
},
},
}),
},
}

for i, tc := range testCases {
t.Run(fmt.Sprintf("%2d-%s", i, tc.testName), func(t *testing.T) {
bodySchema := &schema.BodySchema{
Attributes: tc.attrSchema,
}

f, _ := hclsyntax.ParseConfig([]byte(tc.cfg), "test.tf", hcl.InitialPos)
d := testPathDecoder(t, &PathContext{
Schema: bodySchema,
Files: map[string]*hcl.File{
"test.tf": f,
},
ReferenceTargets: tc.refTargets,
})

ctx := context.Background()
candidates, err := d.CandidatesAtPos(ctx, "test.tf", tc.pos)
if err != nil {
t.Fatal(err)
}

if diff := cmp.Diff(tc.expectedCandidates, candidates); diff != "" {
t.Logf("position: %#v in config: %s", tc.pos, tc.cfg)
t.Fatalf("unexpected candidates: %s", diff)
}
})
}
}
8 changes: 8 additions & 0 deletions decoder/expr_tuple_completion.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,14 @@ func (tuple Tuple) CompletionAtPos(ctx context.Context, pos hcl.Pos) []lang.Cand
if elemExpr.Range().ContainsPos(pos) || elemExpr.Range().End.Byte == pos.Byte {
return newExpression(tuple.pathCtx, elemExpr, tuple.cons.Elems[i]).CompletionAtPos(ctx, pos)
}
if pos.Byte-elemExpr.Range().End.Byte == 1 {
fileBytes := tuple.pathCtx.Files[eType.Range().Filename].Bytes
trailingRune := fileBytes[elemExpr.Range().End.Byte:pos.Byte][0]

if trailingRune == '.' {
return newExpression(tuple.pathCtx, elemExpr, tuple.cons.Elems[i]).CompletionAtPos(ctx, pos)
}
}
lastElemEndPos = elemExpr.Range().End
lastElemIdx = i
}
Expand Down
86 changes: 86 additions & 0 deletions decoder/expr_tuple_completion_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (

"github.com/google/go-cmp/cmp"
"github.com/hashicorp/hcl-lang/lang"
"github.com/hashicorp/hcl-lang/reference"
"github.com/hashicorp/hcl-lang/schema"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
Expand Down Expand Up @@ -761,3 +762,88 @@ func TestCompletionAtPos_exprTuple(t *testing.T) {
})
}
}

func TestCompletionAtPos_exprTuple_references(t *testing.T) {
testCases := []struct {
testName string
attrSchema map[string]*schema.AttributeSchema
refTargets reference.Targets
cfg string
pos hcl.Pos
expectedCandidates lang.Candidates
}{
{
"single-line with trailing dot",
map[string]*schema.AttributeSchema{
"attr": {
Constraint: schema.Tuple{
Elems: []schema.Constraint{
schema.Reference{OfScopeId: lang.ScopeId("variable")},
},
},
},
},
reference.Targets{
{
Addr: lang.Address{
lang.RootStep{Name: "var"},
lang.AttrStep{Name: "bar"},
},
RangePtr: &hcl.Range{
Filename: "variables.tf",
Start: hcl.Pos{Line: 2, Column: 1, Byte: 17},
End: hcl.Pos{Line: 2, Column: 3, Byte: 19},
},
ScopeId: lang.ScopeId("variable"),
},
},
`attr = [ var. ]
`,
hcl.Pos{Line: 1, Column: 14, Byte: 13},
lang.CompleteCandidates([]lang.Candidate{
{
Label: "var.bar",
Detail: "reference",
Kind: lang.TraversalCandidateKind,
TextEdit: lang.TextEdit{
NewText: "var.bar",
Snippet: "var.bar",
Range: hcl.Range{
Filename: "test.tf",
Start: hcl.Pos{Line: 1, Column: 10, Byte: 9},
End: hcl.Pos{Line: 1, Column: 14, Byte: 13},
},
},
},
}),
},
}

for i, tc := range testCases {
t.Run(fmt.Sprintf("%2d-%s", i, tc.testName), func(t *testing.T) {
bodySchema := &schema.BodySchema{
Attributes: tc.attrSchema,
}

f, _ := hclsyntax.ParseConfig([]byte(tc.cfg), "test.tf", hcl.InitialPos)
d := testPathDecoder(t, &PathContext{
Schema: bodySchema,
Files: map[string]*hcl.File{
"test.tf": f,
},
ReferenceTargets: tc.refTargets,
})

ctx := context.Background()
candidates, err := d.CandidatesAtPos(ctx, "test.tf", tc.pos)
if err != nil {
t.Fatal(err)
}

if diff := cmp.Diff(tc.expectedCandidates, candidates); diff != "" {
t.Logf("position: %#v in config: %s", tc.pos, tc.cfg)
t.Fatalf("unexpected candidates: %s", diff)
}
})
}
}

0 comments on commit 0c7d286

Please sign in to comment.