Skip to content

Commit

Permalink
decoder: Implement hover for TypeDeclaration
Browse files Browse the repository at this point in the history
  • Loading branch information
radeksimko committed Feb 17, 2023
1 parent ec6416f commit e2ef0d3
Show file tree
Hide file tree
Showing 3 changed files with 377 additions and 5 deletions.
5 changes: 0 additions & 5 deletions decoder/expr_type_declaration.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,6 @@ type TypeDeclaration struct {
// TODO: optional attribute mode
}

func (td TypeDeclaration) HoverAtPos(ctx context.Context, pos hcl.Pos) *lang.HoverData {
// TODO
return nil
}

func (td TypeDeclaration) SemanticTokens(ctx context.Context) []lang.SemanticToken {
// TODO
return nil
Expand Down
192 changes: 192 additions & 0 deletions decoder/expr_type_declaration_hover.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
package decoder

import (
"context"
"fmt"

"github.com/hashicorp/hcl-lang/lang"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/ext/typeexpr"
"github.com/hashicorp/hcl/v2/hclsyntax"
)

func (td TypeDeclaration) HoverAtPos(ctx context.Context, pos hcl.Pos) *lang.HoverData {
switch eType := td.expr.(type) {
case *hclsyntax.ScopeTraversalExpr:
if len(eType.Traversal) != 1 {
return nil
}

if eType.Range().ContainsPos(pos) {
typ, _ := typeexpr.TypeConstraint(eType)
content, err := hoverContentForType(typ, 0)
if err != nil {
return nil
}
return &lang.HoverData{
Content: lang.Markdown(content),
Range: eType.Range(),
}
}
case *hclsyntax.FunctionCallExpr:
// position in complex type name
if eType.NameRange.ContainsPos(pos) {
typ, diags := typeexpr.TypeConstraint(eType)
if len(diags) > 0 {
return nil
}

content, err := hoverContentForType(typ, 0)
if err != nil {
return nil
}
return &lang.HoverData{
Content: lang.Markdown(content),
Range: eType.Range(),
}
}

// position inside paranthesis
if hcl.RangeBetween(eType.OpenParenRange, eType.CloseParenRange).ContainsPos(pos) {
if isSingleArgTypeName(eType.Name) {
if len(eType.Args) == 0 {
return nil
}

if len(eType.Args) == 1 && eType.Args[0].Range().ContainsPos(pos) {
cons := TypeDeclaration{
expr: eType.Args[0],
pathCtx: td.pathCtx,
}
return cons.HoverAtPos(ctx, pos)
}

return nil
}

if eType.Name == "object" {
if len(eType.Args) != 1 {
return nil
}
objExpr, ok := eType.Args[0].(*hclsyntax.ObjectConsExpr)
if !ok {
return nil
}
if !objExpr.Range().ContainsPos(pos) {
return nil
}

// account for position on {} braces
closeRange := hcl.Range{
Filename: objExpr.Range().Filename,
Start: hcl.Pos{
Line: objExpr.Range().End.Line,
Column: objExpr.Range().End.Column - 1,
Byte: objExpr.Range().End.Byte - 1,
},
End: objExpr.Range().End,
}
if objExpr.OpenRange.ContainsPos(pos) || closeRange.ContainsPos(pos) {
typ, diags := typeexpr.TypeConstraint(eType)
if len(diags) > 0 {
return nil
}
content, err := hoverContentForType(typ, 0)
if err != nil {
return nil
}
return &lang.HoverData{
Content: lang.Markdown(content),
Range: eType.Range(),
}
}

cons := TypeDeclaration{
expr: objExpr,
pathCtx: td.pathCtx,
insideObject: true,
}
return cons.HoverAtPos(ctx, pos)
}

if eType.Name == "tuple" {
if len(eType.Args) != 1 {
return nil
}
tupleExpr, ok := eType.Args[0].(*hclsyntax.TupleConsExpr)
if !ok {
return nil
}
if !tupleExpr.Range().ContainsPos(pos) {
return nil
}

// account for position on [] brackets
closeRange := hcl.Range{
Filename: tupleExpr.Range().Filename,
Start: hcl.Pos{
Line: tupleExpr.Range().End.Line,
Column: tupleExpr.Range().End.Column - 1,
Byte: tupleExpr.Range().End.Byte - 1,
},
End: tupleExpr.Range().End,
}
if tupleExpr.OpenRange.ContainsPos(pos) || closeRange.ContainsPos(pos) {
typ, diags := typeexpr.TypeConstraint(eType)
if len(diags) > 0 {
return nil
}
content, err := hoverContentForType(typ, 0)
if err != nil {
return nil
}
return &lang.HoverData{
Content: lang.Markdown(content),
Range: eType.Range(),
}
}

for _, expr := range tupleExpr.Exprs {
if expr.Range().ContainsPos(pos) {
cons := TypeDeclaration{
expr: expr,
pathCtx: td.pathCtx,
}
return cons.HoverAtPos(ctx, pos)
}
}

return nil
}
}
case *hclsyntax.ObjectConsExpr:
if !td.insideObject {
// reject hover in bare object notation w/out object()
return nil
}

if len(eType.Items) == 0 {
return nil
}

for _, item := range eType.Items {
if item.KeyExpr.Range().ContainsPos(pos) {
val, _ := item.KeyExpr.Value(nil)
typ, _ := typeexpr.TypeConstraint(item.ValueExpr)
return &lang.HoverData{
Content: lang.Markdown(fmt.Sprintf("`%s` = _%s_", val.AsString(), typ.FriendlyNameForConstraint())),
Range: hcl.RangeBetween(item.KeyExpr.Range(), item.ValueExpr.Range()),
}
}
if item.ValueExpr.Range().ContainsPos(pos) {
cons := TypeDeclaration{
expr: item.ValueExpr,
pathCtx: td.pathCtx,
}
return cons.HoverAtPos(ctx, pos)
}
}
return nil
}
return nil
}
185 changes: 185 additions & 0 deletions decoder/expr_type_declaration_hover_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
package decoder

import (
"context"
"fmt"
"testing"

"github.com/google/go-cmp/cmp"
"github.com/hashicorp/hcl-lang/lang"
"github.com/hashicorp/hcl-lang/schema"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
)

func TestHoverAtPos_exprTypeDeclaration(t *testing.T) {
testCases := []struct {
testName string
attrSchema map[string]*schema.AttributeSchema
cfg string
pos hcl.Pos
expectedHoverData *lang.HoverData
}{
{
"primitive type",
map[string]*schema.AttributeSchema{
"attr": {
Constraint: schema.TypeDeclaration{},
},
},
`attr = string`,
hcl.Pos{Line: 1, Column: 11, Byte: 10},
&lang.HoverData{
Content: lang.Markdown(`_string_`),
Range: hcl.Range{
Filename: "test.tf",
Start: hcl.Pos{Line: 1, Column: 8, Byte: 7},
End: hcl.Pos{Line: 1, Column: 14, Byte: 13},
},
},
},
{
"list type on list",
map[string]*schema.AttributeSchema{
"attr": {
Constraint: schema.TypeDeclaration{},
},
},
`attr = list(string)`,
hcl.Pos{Line: 1, Column: 10, Byte: 9},
&lang.HoverData{
Content: lang.Markdown(`_list of string_`),
Range: hcl.Range{
Filename: "test.tf",
Start: hcl.Pos{Line: 1, Column: 8, Byte: 7},
End: hcl.Pos{Line: 1, Column: 20, Byte: 19},
},
},
},
{
"list type on element type",
map[string]*schema.AttributeSchema{
"attr": {
Constraint: schema.TypeDeclaration{},
},
},
`attr = list(string)`,
hcl.Pos{Line: 1, Column: 16, Byte: 15},
&lang.HoverData{
Content: lang.Markdown(`_string_`),
Range: hcl.Range{
Filename: "test.tf",
Start: hcl.Pos{Line: 1, Column: 13, Byte: 12},
End: hcl.Pos{Line: 1, Column: 19, Byte: 18},
},
},
},
{
"tuple type on tuple",
map[string]*schema.AttributeSchema{
"attr": {
Constraint: schema.TypeDeclaration{},
},
},
`attr = tuple([string])`,
hcl.Pos{Line: 1, Column: 10, Byte: 9},
&lang.HoverData{
Content: lang.Markdown(`_tuple_`),
Range: hcl.Range{
Filename: "test.tf",
Start: hcl.Pos{Line: 1, Column: 8, Byte: 7},
End: hcl.Pos{Line: 1, Column: 23, Byte: 22},
},
},
},
{
"object type on object",
map[string]*schema.AttributeSchema{
"attr": {
Constraint: schema.TypeDeclaration{},
},
},
`attr = object({
foo = string
})
`,
hcl.Pos{Line: 1, Column: 11, Byte: 10},
&lang.HoverData{
Content: lang.Markdown("```\n{\n foo = string\n}\n```\n_object_"),
Range: hcl.Range{
Filename: "test.tf",
Start: hcl.Pos{Line: 1, Column: 8, Byte: 7},
End: hcl.Pos{Line: 3, Column: 3, Byte: 33},
},
},
},
{
"object type on attribute name",
map[string]*schema.AttributeSchema{
"attr": {
Constraint: schema.TypeDeclaration{},
},
},
`attr = object({
foo = string
})
`,
hcl.Pos{Line: 2, Column: 5, Byte: 20},
&lang.HoverData{
Content: lang.Markdown("`foo` = _string_"),
Range: hcl.Range{
Filename: "test.tf",
Start: hcl.Pos{Line: 2, Column: 3, Byte: 18},
End: hcl.Pos{Line: 2, Column: 15, Byte: 30},
},
},
},
{
"object type on attribute value",
map[string]*schema.AttributeSchema{
"attr": {
Constraint: schema.TypeDeclaration{},
},
},
`attr = object({
foo = string
})
`,
hcl.Pos{Line: 2, Column: 11, Byte: 26},
&lang.HoverData{
Content: lang.Markdown("_string_"),
Range: hcl.Range{
Filename: "test.tf",
Start: hcl.Pos{Line: 2, Column: 9, Byte: 24},
End: hcl.Pos{Line: 2, Column: 15, Byte: 30},
},
},
},
}

for i, tc := range testCases {
t.Run(fmt.Sprintf("%d-%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,
},
})

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

if diff := cmp.Diff(tc.expectedHoverData, hoverData); diff != "" {
t.Fatalf("unexpected hover data: %s", diff)
}
})
}
}

0 comments on commit e2ef0d3

Please sign in to comment.