diff --git a/decoder/expr_literal_type.go b/decoder/expr_literal_type.go index 49b81270..af3fb135 100644 --- a/decoder/expr_literal_type.go +++ b/decoder/expr_literal_type.go @@ -12,11 +12,8 @@ import ( type LiteralType struct { expr hcl.Expression cons schema.LiteralType -} -func (lt LiteralType) CompletionAtPos(ctx context.Context, pos hcl.Pos) []lang.Candidate { - // TODO - return nil + pathCtx *PathContext } func (lt LiteralType) HoverAtPos(ctx context.Context, pos hcl.Pos) *lang.HoverData { diff --git a/decoder/expr_literal_type_completion.go b/decoder/expr_literal_type_completion.go new file mode 100644 index 00000000..2a7646c4 --- /dev/null +++ b/decoder/expr_literal_type_completion.go @@ -0,0 +1,204 @@ +package decoder + +import ( + "context" + "strings" + + "github.com/hashicorp/hcl-lang/lang" + "github.com/hashicorp/hcl-lang/schema" + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hclsyntax" + "github.com/zclconf/go-cty/cty" +) + +func (lt LiteralType) CompletionAtPos(ctx context.Context, pos hcl.Pos) []lang.Candidate { + typ := lt.cons.Type + + if isEmptyExpression(lt.expr) { + editRange := hcl.Range{ + Filename: lt.expr.Range().Filename, + Start: pos, + End: pos, + } + + if typ.IsPrimitiveType() { + if typ == cty.Bool { + return boolLiteralCandidates("", editRange) + } + return []lang.Candidate{} + } + + if typ == cty.DynamicPseudoType { + return []lang.Candidate{} + } + + return []lang.Candidate{ + { + Label: labelForLiteralType(typ), + Detail: typ.FriendlyName(), + Kind: candidateKindForType(typ), + TextEdit: lang.TextEdit{ + Range: editRange, + NewText: newTextForLiteralType(typ), + Snippet: snippetForLiteralType(1, typ), + }, + }, + } + } + + if typ == cty.Bool { + return lt.completeBoolAtPos(ctx, typ, pos) + } + + if typ.IsListType() { + expr, ok := lt.expr.(*hclsyntax.TupleConsExpr) + if !ok { + return []lang.Candidate{} + } + + cons := schema.List{ + Elem: schema.LiteralType{ + Type: typ.ElementType(), + }, + } + + return newExpression(lt.pathCtx, expr, cons).CompletionAtPos(ctx, pos) + } + + if typ.IsSetType() { + expr, ok := lt.expr.(*hclsyntax.TupleConsExpr) + if !ok { + return []lang.Candidate{} + } + + cons := schema.Set{ + Elem: schema.LiteralType{ + Type: typ.ElementType(), + }, + } + + return newExpression(lt.pathCtx, expr, cons).CompletionAtPos(ctx, pos) + } + + if typ.IsTupleType() { + expr, ok := lt.expr.(*hclsyntax.TupleConsExpr) + if !ok { + return []lang.Candidate{} + } + + elemTypes := typ.TupleElementTypes() + cons := schema.Tuple{ + Elems: make([]schema.Constraint, len(elemTypes)), + } + for i, elemType := range elemTypes { + cons.Elems[i] = schema.LiteralType{ + Type: elemType, + } + } + + return newExpression(lt.pathCtx, expr, cons).CompletionAtPos(ctx, pos) + } + + if typ.IsMapType() { + expr, ok := lt.expr.(*hclsyntax.ObjectConsExpr) + if !ok { + return []lang.Candidate{} + } + + cons := schema.Map{ + Elem: schema.LiteralType{ + Type: typ.ElementType(), + }, + } + return newExpression(lt.pathCtx, expr, cons).CompletionAtPos(ctx, pos) + } + + if typ.IsObjectType() { + expr, ok := lt.expr.(*hclsyntax.ObjectConsExpr) + if !ok { + return []lang.Candidate{} + } + + cons := schema.Object{ + Attributes: ctyObjectToObjectAttributes(typ), + } + return newExpression(lt.pathCtx, expr, cons).CompletionAtPos(ctx, pos) + } + + return nil +} + +func (lt LiteralType) completeBoolAtPos(ctx context.Context, typ cty.Type, pos hcl.Pos) []lang.Candidate { + switch eType := lt.expr.(type) { + + case *hclsyntax.ScopeTraversalExpr: + prefixLen := pos.Byte - eType.Range().Start.Byte + prefix := eType.Traversal.RootName()[0:prefixLen] + return boolLiteralCandidates(prefix, eType.Range()) + + case *hclsyntax.LiteralValueExpr: + if eType.Val.Type() == cty.Bool { + value := "false" + if eType.Val.True() { + value = "true" + } + prefixLen := pos.Byte - eType.Range().Start.Byte + prefix := value[0:prefixLen] + return boolLiteralCandidates(prefix, eType.Range()) + } + } + + return []lang.Candidate{} +} + +func boolLiteralCandidates(prefix string, editRange hcl.Range) []lang.Candidate { + candidates := make([]lang.Candidate, 0) + + if strings.HasPrefix("false", prefix) { + candidates = append(candidates, lang.Candidate{ + Label: "false", + Detail: cty.Bool.FriendlyNameForConstraint(), + Kind: lang.BoolCandidateKind, + TextEdit: lang.TextEdit{ + NewText: "false", + Snippet: "false", + Range: editRange, + }, + }) + } + if strings.HasPrefix("true", prefix) { + candidates = append(candidates, lang.Candidate{ + Label: "true", + Detail: cty.Bool.FriendlyNameForConstraint(), + Kind: lang.BoolCandidateKind, + TextEdit: lang.TextEdit{ + NewText: "true", + Snippet: "true", + Range: editRange, + }, + }) + } + + return candidates +} + +func ctyObjectToObjectAttributes(objType cty.Type) schema.ObjectAttributes { + attrTypes := objType.AttributeTypes() + objAttributes := make(schema.ObjectAttributes, len(attrTypes)) + + for name, attrType := range attrTypes { + aSchema := &schema.AttributeSchema{ + Constraint: schema.LiteralType{ + Type: attrType, + }, + } + if objType.AttributeOptional(name) { + aSchema.IsOptional = true + } else { + aSchema.IsRequired = true + } + objAttributes[name] = aSchema + } + + return objAttributes +} diff --git a/decoder/expr_literal_type_test.go b/decoder/expr_literal_type_test.go new file mode 100644 index 00000000..03475b93 --- /dev/null +++ b/decoder/expr_literal_type_test.go @@ -0,0 +1,1101 @@ +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" + "github.com/zclconf/go-cty/cty" +) + +func TestCompletionAtPos_exprLiteralType(t *testing.T) { + testCases := []struct { + testName string + attrSchema map[string]*schema.AttributeSchema + cfg string + pos hcl.Pos + expectedCandidates lang.Candidates + }{ + { + "bool", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.LiteralType{ + Type: cty.Bool, + }, + }, + }, + `attr = +`, + hcl.Pos{Line: 1, Column: 8, Byte: 7}, + lang.CompleteCandidates([]lang.Candidate{ + { + Label: "false", + Detail: cty.Bool.FriendlyNameForConstraint(), + Kind: lang.BoolCandidateKind, + TextEdit: lang.TextEdit{ + NewText: "false", + Snippet: "false", + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + End: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + }, + }, + }, + { + Label: "true", + Detail: cty.Bool.FriendlyNameForConstraint(), + Kind: lang.BoolCandidateKind, + TextEdit: lang.TextEdit{ + NewText: "true", + Snippet: "true", + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + End: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + }, + }, + }, + }), + }, + { + "bool by prefix", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.LiteralType{ + Type: cty.Bool, + }, + }, + }, + `attr = f +`, + hcl.Pos{Line: 1, Column: 9, Byte: 8}, + lang.CompleteCandidates([]lang.Candidate{ + { + Label: "false", + Detail: cty.Bool.FriendlyNameForConstraint(), + Kind: lang.BoolCandidateKind, + TextEdit: lang.TextEdit{ + NewText: "false", + Snippet: "false", + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + End: hcl.Pos{Line: 1, Column: 9, Byte: 8}, + }, + }, + }, + }), + }, + { + "string", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.LiteralType{ + Type: cty.String, + }, + }, + }, + `attr = +`, + hcl.Pos{Line: 1, Column: 8, Byte: 7}, + lang.CompleteCandidates([]lang.Candidate{}), + }, + { + "list of strings", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.LiteralType{ + Type: cty.List(cty.String), + }, + }, + }, + `attr = +`, + hcl.Pos{Line: 1, Column: 8, Byte: 7}, + lang.CompleteCandidates([]lang.Candidate{ + { + Label: "[ string ]", + Detail: "list of string", + Kind: lang.ListCandidateKind, + TextEdit: lang.TextEdit{ + NewText: `[ "" ]`, + Snippet: `[ "${1:value}" ]`, + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + End: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + }, + }, + }, + }), + }, + { + "inside list of bool", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.LiteralType{ + Type: cty.List(cty.Bool), + }, + }, + }, + `attr = [ ] +`, + hcl.Pos{Line: 1, Column: 10, Byte: 9}, + lang.CompleteCandidates(boolLiteralCandidates("", hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 10, Byte: 9}, + End: hcl.Pos{Line: 1, Column: 10, Byte: 9}, + })), + }, + { + "inside list of bool multiline", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.LiteralType{ + Type: cty.List(cty.Bool), + }, + }, + }, + `attr = [ + +] +`, + hcl.Pos{Line: 2, Column: 3, Byte: 11}, + lang.CompleteCandidates(boolLiteralCandidates("", hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 2, Column: 3, Byte: 11}, + End: hcl.Pos{Line: 2, Column: 3, Byte: 11}, + })), + }, + { + "inside list next element after space", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.LiteralType{ + Type: cty.List(cty.Bool), + }, + }, + }, + `attr = [ false, ] +`, + hcl.Pos{Line: 1, Column: 17, Byte: 16}, + lang.CompleteCandidates(boolLiteralCandidates("", hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 17, Byte: 16}, + End: hcl.Pos{Line: 1, Column: 17, Byte: 16}, + })), + }, + { + "inside list next element after newline", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.LiteralType{ + Type: cty.List(cty.Bool), + }, + }, + }, + `attr = [ + false, + +] +`, + hcl.Pos{Line: 3, Column: 3, Byte: 20}, + lang.CompleteCandidates(boolLiteralCandidates("", hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 3, Column: 3, Byte: 20}, + End: hcl.Pos{Line: 3, Column: 3, Byte: 20}, + })), + }, + { + "inside list next element after comma", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.LiteralType{ + Type: cty.List(cty.Bool), + }, + }, + }, + `attr = [ false, ] +`, + hcl.Pos{Line: 1, Column: 16, Byte: 15}, + lang.CompleteCandidates(boolLiteralCandidates("", hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 16, Byte: 15}, + End: hcl.Pos{Line: 1, Column: 16, Byte: 15}, + })), + }, + { + "inside list next element near closing bracket", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.LiteralType{ + Type: cty.List(cty.Bool), + }, + }, + }, + `attr = [ false, ] +`, + hcl.Pos{Line: 1, Column: 17, Byte: 16}, + lang.CompleteCandidates(boolLiteralCandidates("", hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 17, Byte: 16}, + End: hcl.Pos{Line: 1, Column: 17, Byte: 16}, + })), + }, + { + "completion inside list with prefix", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.LiteralType{ + Type: cty.List(cty.Bool), + }, + }, + }, + `attr = [ f ] +`, + hcl.Pos{Line: 1, Column: 11, Byte: 10}, + lang.CompleteCandidates([]lang.Candidate{ + { + Label: "false", + Detail: cty.Bool.FriendlyNameForConstraint(), + Kind: lang.BoolCandidateKind, + TextEdit: lang.TextEdit{ + NewText: "false", + Snippet: "false", + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 10, Byte: 9}, + End: hcl.Pos{Line: 1, Column: 11, Byte: 10}, + }, + }, + }, + }), + }, + { + "tuple", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.LiteralType{ + Type: cty.Tuple([]cty.Type{cty.Bool}), + }, + }, + }, + `attr = +`, + hcl.Pos{Line: 1, Column: 8, Byte: 7}, + lang.CompleteCandidates([]lang.Candidate{ + { + Label: "[ bool ]", + Detail: "tuple", + Kind: lang.TupleCandidateKind, + TextEdit: lang.TextEdit{ + NewText: "[ false ]", + Snippet: "[ ${1:false} ]", + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + End: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + }, + }, + }, + }), + }, + { + "inside tuple", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.LiteralType{ + Type: cty.Tuple([]cty.Type{cty.Bool}), + }, + }, + }, + `attr = [ ] +`, + hcl.Pos{Line: 1, Column: 10, Byte: 9}, + lang.CompleteCandidates([]lang.Candidate{ + { + Label: "false", + Detail: "bool", + Kind: lang.BoolCandidateKind, + TextEdit: lang.TextEdit{ + NewText: "false", + Snippet: "false", + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 10, Byte: 9}, + End: hcl.Pos{Line: 1, Column: 10, Byte: 9}, + }, + }, + }, + { + Label: "true", + Detail: "bool", + Kind: lang.BoolCandidateKind, + TextEdit: lang.TextEdit{ + NewText: "true", + Snippet: "true", + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 10, Byte: 9}, + End: hcl.Pos{Line: 1, Column: 10, Byte: 9}, + }, + }, + }, + }), + }, + { + "inside tuple next element", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.LiteralType{ + Type: cty.Tuple([]cty.Type{cty.String, cty.Bool}), + }, + }, + }, + `attr = [ "", ] +`, + hcl.Pos{Line: 1, Column: 14, Byte: 13}, + lang.CompleteCandidates([]lang.Candidate{ + { + Label: "false", + Detail: "bool", + Kind: lang.BoolCandidateKind, + TextEdit: lang.TextEdit{ + NewText: "false", + Snippet: "false", + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 14, Byte: 13}, + End: hcl.Pos{Line: 1, Column: 14, Byte: 13}, + }, + }, + }, + { + Label: "true", + Detail: "bool", + Kind: lang.BoolCandidateKind, + TextEdit: lang.TextEdit{ + NewText: "true", + Snippet: "true", + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 14, Byte: 13}, + End: hcl.Pos{Line: 1, Column: 14, Byte: 13}, + }, + }, + }, + }), + }, + { + "inside tuple next element without comma", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.LiteralType{ + Type: cty.Tuple([]cty.Type{cty.String, cty.Bool}), + }, + }, + }, + `attr = [ "" ] +`, + hcl.Pos{Line: 1, Column: 13, Byte: 12}, + lang.CompleteCandidates([]lang.Candidate{}), + }, + { + "inside tuple in space between elements", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.LiteralType{ + Type: cty.Tuple([]cty.Type{cty.String, cty.String, cty.Bool}), + }, + }, + }, + `attr = [ "", "" ] +`, + hcl.Pos{Line: 1, Column: 13, Byte: 12}, + lang.CompleteCandidates([]lang.Candidate{}), + }, + { + "inside tuple next element which does not exist", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.LiteralType{ + Type: cty.Tuple([]cty.Type{cty.String}), + }, + }, + }, + `attr = [ "", ] +`, + hcl.Pos{Line: 1, Column: 14, Byte: 13}, + lang.CompleteCandidates([]lang.Candidate{}), + }, + { + "map", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.LiteralType{ + Type: cty.Map(cty.Bool), + }, + }, + }, + `attr = +`, + hcl.Pos{Line: 1, Column: 8, Byte: 7}, + lang.CompleteCandidates([]lang.Candidate{ + { + Label: `{ "key" = bool }`, + Detail: "map of bool", + Kind: lang.MapCandidateKind, + TextEdit: lang.TextEdit{ + NewText: "{\n \"key\" = false\n}", + Snippet: "{\n \"${1:key}\" = ${2:false}\n}", + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + End: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + }, + }, + }, + }), + }, + { + "inside empty map", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.LiteralType{ + Type: cty.Map(cty.Bool), + }, + }, + }, + `attr = { + +} +`, + hcl.Pos{Line: 2, Column: 3, Byte: 11}, + lang.CompleteCandidates([]lang.Candidate{ + { + Label: `"key" = bool`, + Detail: "bool", + Kind: lang.AttributeCandidateKind, + TextEdit: lang.TextEdit{ + NewText: "\"key\" = false", + Snippet: "\"${1:key}\" = ${2:false}", + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 2, Column: 3, Byte: 11}, + End: hcl.Pos{Line: 2, Column: 3, Byte: 11}, + }, + }, + }, + }), + }, + { + "inside map after first item", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.LiteralType{ + Type: cty.Map(cty.Bool), + }, + }, + }, + `attr = { + "key" = true + +} +`, + hcl.Pos{Line: 3, Column: 3, Byte: 26}, + lang.CompleteCandidates([]lang.Candidate{ + { + Label: `"key" = bool`, + Detail: "bool", + Kind: lang.AttributeCandidateKind, + TextEdit: lang.TextEdit{ + NewText: "\"key\" = false", + Snippet: "\"${1:key}\" = ${2:false}", + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 3, Column: 3, Byte: 26}, + End: hcl.Pos{Line: 3, Column: 3, Byte: 26}, + }, + }, + }, + }), + }, + { + "inside map between items", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.LiteralType{ + Type: cty.Map(cty.Bool), + }, + }, + }, + `attr = { + "key" = true + + "another" = false +} +`, + hcl.Pos{Line: 3, Column: 3, Byte: 26}, + lang.CompleteCandidates([]lang.Candidate{ + { + Label: `"key" = bool`, + Detail: "bool", + Kind: lang.AttributeCandidateKind, + TextEdit: lang.TextEdit{ + NewText: "\"key\" = false", + Snippet: "\"${1:key}\" = ${2:false}", + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 3, Column: 3, Byte: 26}, + End: hcl.Pos{Line: 3, Column: 3, Byte: 26}, + }, + }, + }, + }), + }, + { + "inside map before item", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.LiteralType{ + Type: cty.Map(cty.Bool), + }, + }, + }, + `attr = { + "key" = true +} +`, + hcl.Pos{Line: 2, Column: 2, Byte: 10}, + lang.CompleteCandidates([]lang.Candidate{ + { + Label: `"key" = bool`, + Detail: "bool", + TextEdit: lang.TextEdit{ + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 2, Column: 2, Byte: 10}, + End: hcl.Pos{Line: 2, Column: 2, Byte: 10}, + }, + NewText: `"key" = false`, + Snippet: `"${1:key}" = ${2:false}`, + }, + Kind: lang.AttributeCandidateKind, + }, + }), + }, + { + "inside map value empty", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.LiteralType{ + Type: cty.Map(cty.Bool), + }, + }, + }, + `attr = { + "key" = +} +`, + hcl.Pos{Line: 2, Column: 11, Byte: 19}, + lang.CompleteCandidates([]lang.Candidate{ + { + Label: "false", + Detail: "bool", + Kind: lang.BoolCandidateKind, + TextEdit: lang.TextEdit{ + NewText: "false", + Snippet: "false", + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 2, Column: 11, Byte: 19}, + End: hcl.Pos{Line: 2, Column: 11, Byte: 19}, + }, + }, + }, + { + Label: "true", + Detail: "bool", + Kind: lang.BoolCandidateKind, + TextEdit: lang.TextEdit{ + NewText: "true", + Snippet: "true", + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 2, Column: 11, Byte: 19}, + End: hcl.Pos{Line: 2, Column: 11, Byte: 19}, + }, + }, + }, + }), + }, + { + "inside map value with prefix", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.LiteralType{ + Type: cty.Map(cty.Bool), + }, + }, + }, + `attr = { + "key" = f +} +`, + hcl.Pos{Line: 2, Column: 12, Byte: 20}, + lang.CompleteCandidates([]lang.Candidate{ + { + Label: "false", + Detail: "bool", + Kind: lang.BoolCandidateKind, + TextEdit: lang.TextEdit{ + NewText: "false", + Snippet: "false", + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 2, Column: 11, Byte: 19}, + End: hcl.Pos{Line: 2, Column: 12, Byte: 20}, + }, + }, + }, + }), + }, + { + "object", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.LiteralType{ + Type: cty.Object(map[string]cty.Type{ + "foo": cty.String, + "bar": cty.Bool, + "baz": cty.Number, + }), + }, + }, + }, + `attr = +`, + hcl.Pos{Line: 1, Column: 8, Byte: 7}, + lang.CompleteCandidates([]lang.Candidate{ + { + Label: `{ bar = bool, … }`, + Detail: "object", + Kind: lang.ObjectCandidateKind, + TextEdit: lang.TextEdit{ + NewText: "{\n bar = false\n baz = 1\n foo = \"\"\n}", + Snippet: "{\n bar = ${1:false}\n baz = ${2:1}\n foo = \"${3:value}\"\n}", + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + End: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + }, + }, + }, + }), + }, + { + "inside empty object", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.LiteralType{ + Type: cty.Object(map[string]cty.Type{ + "foo": cty.String, + "bar": cty.Bool, + "baz": cty.Number, + }), + }, + }, + }, + `attr = { + +} +`, + hcl.Pos{Line: 2, Column: 1, Byte: 9}, + lang.CompleteCandidates([]lang.Candidate{ + { + Label: `bar`, + Detail: "required, bool", + Kind: lang.AttributeCandidateKind, + TextEdit: lang.TextEdit{ + NewText: "bar", + Snippet: "bar = ${1:false}", + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 2, Column: 1, Byte: 9}, + End: hcl.Pos{Line: 2, Column: 1, Byte: 9}, + }, + }, + }, + { + Label: `baz`, + Detail: "required, number", + Kind: lang.AttributeCandidateKind, + TextEdit: lang.TextEdit{ + NewText: "baz", + Snippet: "baz = ${1:0}", + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 2, Column: 1, Byte: 9}, + End: hcl.Pos{Line: 2, Column: 1, Byte: 9}, + }, + }, + }, + { + Label: `foo`, + Detail: "required, string", + Kind: lang.AttributeCandidateKind, + TextEdit: lang.TextEdit{ + NewText: "foo", + Snippet: "foo = \"${1:value}\"", + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 2, Column: 1, Byte: 9}, + End: hcl.Pos{Line: 2, Column: 1, Byte: 9}, + }, + }, + }, + }), + }, + { + "inside object after first item", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.LiteralType{ + Type: cty.Object(map[string]cty.Type{ + "foo": cty.String, + "bar": cty.Bool, + }), + }, + }, + }, + `attr = { + foo = "baz" + +} +`, + hcl.Pos{Line: 3, Column: 3, Byte: 25}, + lang.CompleteCandidates([]lang.Candidate{ + { + Label: `bar`, + Detail: "required, bool", + Kind: lang.AttributeCandidateKind, + TextEdit: lang.TextEdit{ + NewText: "bar", + Snippet: "bar = ${1:false}", + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 3, Column: 3, Byte: 25}, + End: hcl.Pos{Line: 3, Column: 3, Byte: 25}, + }, + }, + }, + }), + }, + { + "inside object between items", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.LiteralType{ + Type: cty.Object(map[string]cty.Type{ + "foo": cty.String, + "bar": cty.Bool, + "baz": cty.Number, + }), + }, + }, + }, + `attr = { + foo = "baz" + + baz = 42 +} +`, + hcl.Pos{Line: 3, Column: 3, Byte: 25}, + lang.CompleteCandidates([]lang.Candidate{ + { + Label: `bar`, + Detail: "required, bool", + Kind: lang.AttributeCandidateKind, + TextEdit: lang.TextEdit{ + NewText: "bar", + Snippet: "bar = ${1:false}", + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 3, Column: 3, Byte: 25}, + End: hcl.Pos{Line: 3, Column: 3, Byte: 25}, + }, + }, + }, + }), + }, + { + "inside object before item", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.LiteralType{ + Type: cty.Object(map[string]cty.Type{ + "foo": cty.String, + "bar": cty.Bool, + }), + }, + }, + }, + `attr = { + foo = "baz" +} +`, + hcl.Pos{Line: 2, Column: 2, Byte: 10}, + lang.CompleteCandidates([]lang.Candidate{}), + }, + { + "inside object key", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.LiteralType{ + Type: cty.Object(map[string]cty.Type{ + "foo": cty.String, + "bar": cty.Bool, + "baz": cty.Number, + }), + }, + }, + }, + `attr = { + bar = true +} +`, + hcl.Pos{Line: 2, Column: 5, Byte: 13}, + lang.CompleteCandidates([]lang.Candidate{ + { + Label: `bar`, + Detail: "required, bool", + Kind: lang.AttributeCandidateKind, + TextEdit: lang.TextEdit{ + NewText: "bar", + Snippet: "bar = ${1:false}", + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 2, Column: 3, Byte: 11}, + End: hcl.Pos{Line: 2, Column: 13, Byte: 21}, + }, + }, + }, + { + Label: `baz`, + Detail: "required, number", + Kind: lang.AttributeCandidateKind, + TextEdit: lang.TextEdit{ + NewText: "baz", + Snippet: "baz = ${1:0}", + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 2, Column: 3, Byte: 11}, + End: hcl.Pos{Line: 2, Column: 13, Byte: 21}, + }, + }, + }, + }), + }, + { + "inside object value", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.LiteralType{ + Type: cty.Object(map[string]cty.Type{ + "foo": cty.String, + "bar": cty.Bool, + "baz": cty.Number, + }), + }, + }, + }, + `attr = { + bar = false +} +`, + hcl.Pos{Line: 2, Column: 10, Byte: 18}, + lang.CompleteCandidates([]lang.Candidate{ + { + Label: `false`, + Detail: "bool", + Kind: lang.BoolCandidateKind, + TextEdit: lang.TextEdit{ + NewText: "false", + Snippet: "false", + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 2, Column: 9, Byte: 17}, + End: hcl.Pos{Line: 2, Column: 14, Byte: 22}, + }, + }, + }, + }), + }, + { + "inside object with incomplete key", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.LiteralType{ + Type: cty.Object(map[string]cty.Type{ + "foo": cty.String, + "bar": cty.Bool, + "baz": cty.Number, + }), + }, + }, + }, + `attr = { + ba +} +`, + hcl.Pos{Line: 2, Column: 5, Byte: 13}, + lang.CompleteCandidates([]lang.Candidate{ + { + Label: `bar`, + Detail: "required, bool", + Kind: lang.AttributeCandidateKind, + TextEdit: lang.TextEdit{ + NewText: "bar", + Snippet: "bar = ${1:false}", + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 2, Column: 3, Byte: 11}, + End: hcl.Pos{Line: 2, Column: 5, Byte: 13}, + }, + }, + }, + { + Label: `baz`, + Detail: "required, number", + Kind: lang.AttributeCandidateKind, + TextEdit: lang.TextEdit{ + NewText: "baz", + Snippet: "baz = ${1:0}", + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 2, Column: 3, Byte: 11}, + End: hcl.Pos{Line: 2, Column: 5, Byte: 13}, + }, + }, + }, + }), + }, + { + "inside object with no value", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.LiteralType{ + Type: cty.Object(map[string]cty.Type{ + "foo": cty.String, + "bar": cty.Bool, + "baz": cty.Number, + }), + }, + }, + }, + `attr = { + bar = +} +`, + hcl.Pos{Line: 2, Column: 9, Byte: 17}, + lang.CompleteCandidates([]lang.Candidate{ + { + Label: `false`, + Detail: "bool", + Kind: lang.BoolCandidateKind, + TextEdit: lang.TextEdit{ + NewText: "false", + Snippet: "false", + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 2, Column: 9, Byte: 17}, + End: hcl.Pos{Line: 2, Column: 9, Byte: 17}, + }, + }, + }, + { + Label: `true`, + Detail: "bool", + Kind: lang.BoolCandidateKind, + TextEdit: lang.TextEdit{ + NewText: "true", + Snippet: "true", + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 2, Column: 9, Byte: 17}, + End: hcl.Pos{Line: 2, Column: 9, Byte: 17}, + }, + }, + }, + }), + }, + { + "inside object with incomplete value", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.LiteralType{ + Type: cty.Object(map[string]cty.Type{ + "foo": cty.String, + "bar": cty.Bool, + "baz": cty.Number, + }), + }, + }, + }, + `attr = { + bar = f +} +`, + hcl.Pos{Line: 2, Column: 10, Byte: 18}, + lang.CompleteCandidates([]lang.Candidate{ + { + Label: `false`, + Detail: "bool", + Kind: lang.BoolCandidateKind, + TextEdit: lang.TextEdit{ + NewText: "false", + Snippet: "false", + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 2, Column: 9, Byte: 17}, + End: hcl.Pos{Line: 2, Column: 10, Byte: 18}, + }, + }, + }, + }), + }, + } + + 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() + candidates, err := d.CandidatesAtPos(ctx, "test.tf", tc.pos) + if err != nil { + t.Fatal(err) + } + + if diff := cmp.Diff(tc.expectedCandidates, candidates); diff != "" { + t.Fatalf("unexpected candidates: %s", diff) + } + }) + } +} diff --git a/decoder/expression.go b/decoder/expression.go index 4b652be9..a09a59d1 100644 --- a/decoder/expression.go +++ b/decoder/expression.go @@ -105,8 +105,9 @@ func newExpression(pathContext *PathContext, expr hcl.Expression, cons schema.Co } case schema.LiteralType: return LiteralType{ - expr: expr, - cons: c, + expr: expr, + cons: c, + pathCtx: pathContext, } case schema.LiteralValue: return LiteralValue{