Skip to content

Commit

Permalink
decoder/schema: Implement completion for TypeDeclaration
Browse files Browse the repository at this point in the history
  • Loading branch information
radeksimko committed Jan 17, 2023
1 parent d85c2db commit 7bf0496
Show file tree
Hide file tree
Showing 3 changed files with 688 additions and 4 deletions.
322 changes: 320 additions & 2 deletions decoder/expr_type_declaration.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,339 @@ package decoder

import (
"context"
"fmt"
"log"
"strings"

"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"
"github.com/zclconf/go-cty/cty"
)

type TypeDeclaration struct {
expr hcl.Expression
cons schema.TypeDeclaration

insideObject bool

// TODO: optional attribute mode
}

func (td TypeDeclaration) CompletionAtPos(ctx context.Context, pos hcl.Pos) []lang.Candidate {
// TODO
return nil
log.Printf("expression: %#v", td.expr)
if isEmptyExpression(td.expr) || td.expr == nil {
editRange := hcl.Range{
Filename: td.expr.Range().Filename,
Start: pos,
End: pos,
}
return allTypeDeclarationsAsCandidates("", editRange)
}

switch eType := td.expr.(type) {
case *hclsyntax.ScopeTraversalExpr:
if len(eType.Traversal) != 1 {
return []lang.Candidate{}
}

prefixLen := pos.Byte - eType.Range().Start.Byte
prefix := eType.Traversal.RootName()[0:prefixLen]

editRange := hcl.Range{
Filename: eType.Range().Filename,
Start: eType.Range().Start,
End: eType.Range().End,
}

return allTypeDeclarationsAsCandidates(prefix, editRange)
case *hclsyntax.FunctionCallExpr:
// position in complex type name
if eType.NameRange.ContainsPos(pos) {
prefixLen := pos.Byte - eType.NameRange.Start.Byte
prefix := eType.Name[0:prefixLen]

editRange := eType.Range()
return allTypeDeclarationsAsCandidates(prefix, editRange)
}

// position inside paranthesis
if hcl.RangeBetween(eType.OpenParenRange, eType.CloseParenRange).ContainsPos(pos) {
isSingleArgType := eType.Name == "list" || eType.Name == "set" || eType.Name == "map"

// single argument types
if isSingleArgType {
if len(eType.Args) == 0 {
editRange := hcl.Range{
Filename: eType.Range().Filename,
Start: eType.OpenParenRange.End,
End: eType.CloseParenRange.Start,
}

return allTypeDeclarationsAsCandidates("", editRange)
}

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

return []lang.Candidate{}
}

// object type
if eType.Name == "object" {
if len(eType.Args) == 0 {
editRange := hcl.Range{
Filename: eType.Range().Filename,
Start: eType.OpenParenRange.End,
End: eType.CloseParenRange.Start,
}

return innerObjectTypeAsCompletionCandidates(editRange)
}

if len(eType.Args) == 1 {
objExpr, isObject := eType.Args[0].(*hclsyntax.ObjectConsExpr)
if !isObject {
return []lang.Candidate{}
}
if !eType.Args[0].Range().ContainsPos(pos) {
return []lang.Candidate{}
}

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

return []lang.Candidate{}
}

// multi argument type (tuple)
if eType.Name == "tuple" {
if len(eType.Args) == 0 {
editRange := hcl.Range{
Filename: eType.Range().Filename,
Start: eType.OpenParenRange.End,
End: eType.CloseParenRange.Start,
}

return allTypeDeclarationsAsCandidates("", editRange)
}

for _, expr := range eType.Args {
if expr.Range().ContainsPos(pos) || expr.Range().End.Byte == pos.Byte {
cons := TypeDeclaration{
expr: expr,
}
return cons.CompletionAtPos(ctx, pos)
}
}

betweenParens := hcl.Range{
Filename: eType.Range().Filename,
Start: eType.OpenParenRange.End,
End: eType.CloseParenRange.Start,
}
if betweenParens.ContainsPos(pos) || betweenParens.End.Byte == pos.Byte {
editRange := hcl.Range{
Filename: eType.Range().Filename,
Start: pos,
End: pos,
}
return allTypeDeclarationsAsCandidates("", editRange)
}

return []lang.Candidate{}
}
}
case *hclsyntax.ObjectConsExpr:
if !td.insideObject {
// reject completion in bare object notation w/out object()
return []lang.Candidate{}
}

if len(eType.Items) == 0 {
editRange := hcl.Range{
Filename: eType.Range().Filename,
Start: pos,
End: pos,
}

candidates := make([]lang.Candidate, 0)
candidates = append(candidates, lang.Candidate{
Label: "name = type",
Detail: "attribute",
Kind: lang.AttributeCandidateKind,
TextEdit: lang.TextEdit{
NewText: "name = ",
Snippet: fmt.Sprintf("${%d:name} = ${%d}", 1, 2),
Range: editRange,
},
})
return candidates
}

for _, item := range eType.Items {
if item.KeyExpr.Range().ContainsPos(pos) {
return []lang.Candidate{}
}
if item.ValueExpr.Range().ContainsPos(pos) || item.ValueExpr.Range().End.Byte == pos.Byte {
cons := TypeDeclaration{
expr: item.ValueExpr,
}
return cons.CompletionAtPos(ctx, pos)
}
}

return []lang.Candidate{}
}

return []lang.Candidate{}
}

func allTypeDeclarationsAsCandidates(prefix string, editRange hcl.Range) []lang.Candidate {
candidates := make([]lang.Candidate, 0)
candidates = append(candidates, primitiveTypeDeclarationsAsCandidates(prefix, editRange)...)
candidates = append(candidates, complexTypeDeclarationsAsCandidates(prefix, editRange)...)
return candidates
}

func primitiveTypeDeclarationsAsCandidates(prefix string, editRange hcl.Range) []lang.Candidate {
candidates := make([]lang.Candidate, 0)

if strings.HasPrefix("bool", prefix) {
candidates = append(candidates, lang.Candidate{
Label: cty.Bool.FriendlyNameForConstraint(),
Detail: cty.Bool.FriendlyNameForConstraint(),
Kind: lang.BoolCandidateKind,
TextEdit: lang.TextEdit{
NewText: "bool",
Snippet: "bool",
Range: editRange,
},
})
}
if strings.HasPrefix("number", prefix) {
candidates = append(candidates, lang.Candidate{
Label: cty.Number.FriendlyNameForConstraint(),
Detail: cty.Number.FriendlyNameForConstraint(),
Kind: lang.NumberCandidateKind,
TextEdit: lang.TextEdit{
NewText: "number",
Snippet: "number",
Range: editRange,
},
})
}
if strings.HasPrefix("string", prefix) {
candidates = append(candidates, lang.Candidate{
Label: cty.String.FriendlyNameForConstraint(),
Detail: cty.String.FriendlyNameForConstraint(),
Kind: lang.StringCandidateKind,
TextEdit: lang.TextEdit{
NewText: "string",
Snippet: "string",
Range: editRange,
},
})
}

return candidates
}

func complexTypeDeclarationsAsCandidates(prefix string, editRange hcl.Range) []lang.Candidate {
candidates := make([]lang.Candidate, 0)
// TODO: indentation

if strings.HasPrefix("list", prefix) {
candidates = append(candidates, lang.Candidate{
Label: "list(...)",
Detail: "list",
Kind: lang.ListCandidateKind,
TextEdit: lang.TextEdit{
NewText: "list()",
Snippet: fmt.Sprintf("list(${%d})", 0),
Range: editRange,
},
TriggerSuggest: true,
})
}
if strings.HasPrefix("set", prefix) {
candidates = append(candidates, lang.Candidate{
Label: "set(...)",
Detail: "set",
Kind: lang.SetCandidateKind,
TextEdit: lang.TextEdit{
NewText: "set()",
Snippet: fmt.Sprintf("set(${%d})", 0),
Range: editRange,
},
TriggerSuggest: true,
})
}
if strings.HasPrefix("tuple", prefix) {
candidates = append(candidates, lang.Candidate{
Label: "tuple(...)",
Detail: "tuple",
Kind: lang.TupleCandidateKind,
TextEdit: lang.TextEdit{
NewText: "tuple()",
Snippet: fmt.Sprintf("tuple(${%d})", 0),
Range: editRange,
},
TriggerSuggest: true,
})
}
if strings.HasPrefix("map", prefix) {
candidates = append(candidates, lang.Candidate{
Label: "map(...)",
Detail: "map",
Kind: lang.MapCandidateKind,
TextEdit: lang.TextEdit{
NewText: "map()",
Snippet: fmt.Sprintf("map(${%d})", 0),
Range: editRange,
},
TriggerSuggest: true,
})
}
if strings.HasPrefix("object", prefix) {
candidates = append(candidates, lang.Candidate{
Label: "object({...})",
Detail: "object",
Kind: lang.ObjectCandidateKind,
TextEdit: lang.TextEdit{
NewText: "object({\n\n})",
Snippet: fmt.Sprintf("object({\n ${%d:name} = ${%d}\n})", 1, 2),
Range: editRange,
},
})
}

return candidates
}

func innerObjectTypeAsCompletionCandidates(editRange hcl.Range) []lang.Candidate {
return []lang.Candidate{
{
Label: "{...}",
Detail: "object",
Kind: lang.ObjectCandidateKind,
TextEdit: lang.TextEdit{
NewText: "{\n\n}",
Snippet: fmt.Sprintf("{\n ${%d:name} = ${%d}\n}", 1, 2),
Range: editRange,
},
},
}
}

func (td TypeDeclaration) HoverAtPos(ctx context.Context, pos hcl.Pos) *lang.HoverData {
Expand Down
Loading

0 comments on commit 7bf0496

Please sign in to comment.