diff --git a/decoder/attribute_candidates_legacy_test.go b/decoder/attribute_candidates_legacy_test.go index 0d9b5ca1..bee82d92 100644 --- a/decoder/attribute_candidates_legacy_test.go +++ b/decoder/attribute_candidates_legacy_test.go @@ -24,7 +24,7 @@ func TestLegacyDecoder_CompletionAtPos_EmptyCompletionData(t *testing.T) { "primitive type", "primitive", &schema.AttributeSchema{ - Expr: schema.LiteralTypeOnly(cty.String), + Constraint: schema.LiteralType{Type: cty.String}, }, lang.CompleteCandidates([]lang.Candidate{ { @@ -47,7 +47,7 @@ func TestLegacyDecoder_CompletionAtPos_EmptyCompletionData(t *testing.T) { "map of strings", "mymap", &schema.AttributeSchema{ - Expr: schema.LiteralTypeOnly(cty.Map(cty.String)), + Constraint: schema.LiteralType{Type: cty.Map(cty.String)}, }, lang.CompleteCandidates([]lang.Candidate{ { @@ -72,7 +72,7 @@ func TestLegacyDecoder_CompletionAtPos_EmptyCompletionData(t *testing.T) { "map of numbers", "mymap", &schema.AttributeSchema{ - Expr: schema.LiteralTypeOnly(cty.Map(cty.Number)), + Constraint: schema.LiteralType{Type: cty.Map(cty.Number)}, }, lang.CompleteCandidates([]lang.Candidate{ { @@ -97,7 +97,7 @@ func TestLegacyDecoder_CompletionAtPos_EmptyCompletionData(t *testing.T) { "list of numbers", "mylist", &schema.AttributeSchema{ - Expr: schema.LiteralTypeOnly(cty.List(cty.Number)), + Constraint: schema.LiteralType{Type: cty.List(cty.Number)}, }, lang.CompleteCandidates([]lang.Candidate{ { @@ -120,10 +120,10 @@ func TestLegacyDecoder_CompletionAtPos_EmptyCompletionData(t *testing.T) { "list of objects", "mylistobj", &schema.AttributeSchema{ - Expr: schema.LiteralTypeOnly(cty.List(cty.Object(map[string]cty.Type{ + Constraint: schema.LiteralType{Type: cty.List(cty.Object(map[string]cty.Type{ "first": cty.String, "second": cty.Number, - }))), + }))}, }, lang.CompleteCandidates([]lang.Candidate{ { @@ -149,7 +149,7 @@ func TestLegacyDecoder_CompletionAtPos_EmptyCompletionData(t *testing.T) { "set of numbers", "myset", &schema.AttributeSchema{ - Expr: schema.LiteralTypeOnly(cty.Set(cty.Number)), + Constraint: schema.LiteralType{Type: cty.Set(cty.Number)}, }, lang.CompleteCandidates([]lang.Candidate{ { @@ -172,11 +172,11 @@ func TestLegacyDecoder_CompletionAtPos_EmptyCompletionData(t *testing.T) { "object", "myobj", &schema.AttributeSchema{ - Expr: schema.LiteralTypeOnly(cty.Object(map[string]cty.Type{ + Constraint: schema.LiteralType{Type: cty.Object(map[string]cty.Type{ "keystr": cty.String, "keynum": cty.Number, "keybool": cty.Bool, - })), + })}, }, lang.CompleteCandidates([]lang.Candidate{ { @@ -203,7 +203,7 @@ func TestLegacyDecoder_CompletionAtPos_EmptyCompletionData(t *testing.T) { "unknown type", "mynil", &schema.AttributeSchema{ - Expr: schema.LiteralTypeOnly(cty.DynamicPseudoType), + Constraint: schema.LiteralType{Type: cty.DynamicPseudoType}, }, lang.CompleteCandidates([]lang.Candidate{ { @@ -226,13 +226,13 @@ func TestLegacyDecoder_CompletionAtPos_EmptyCompletionData(t *testing.T) { "nested object", "myobj", &schema.AttributeSchema{ - Expr: schema.LiteralTypeOnly(cty.Object(map[string]cty.Type{ + Constraint: schema.LiteralType{Type: cty.Object(map[string]cty.Type{ "keystr": cty.String, "another": cty.Object(map[string]cty.Type{ "nestedstr": cty.String, "nested_number": cty.Number, }), - })), + })}, }, lang.CompleteCandidates([]lang.Candidate{ { diff --git a/schema/constraint_literal_type.go b/schema/constraint_literal_type.go index 99016fce..0680a506 100644 --- a/schema/constraint_literal_type.go +++ b/schema/constraint_literal_type.go @@ -1,6 +1,10 @@ package schema import ( + "fmt" + "sort" + "strings" + "github.com/zclconf/go-cty/cty" ) @@ -16,6 +20,10 @@ import ( // e.g. LiteralType{Type: cty.List(...)}. type LiteralType struct { Type cty.Type + + // nestingLvl represents current level of nesting + // used for correctly indenting complex types. + nestingLvl int } func (LiteralType) isConstraintImpl() constraintSigil { @@ -28,11 +36,158 @@ func (lt LiteralType) FriendlyName() string { func (lt LiteralType) Copy() Constraint { return LiteralType{ - Type: lt.Type, + Type: lt.Type, + nestingLvl: lt.nestingLvl, } } func (lt LiteralType) EmptyCompletionData(nextPlaceholder int) CompletionData { - // TODO - return CompletionData{} + // primitive types + switch lt.Type { + case cty.String: + return CompletionData{ + NewText: `""`, + Snippet: fmt.Sprintf(`"${%d:value}"`, nextPlaceholder), + LastPlaceholder: nextPlaceholder, + } + case cty.Bool: + return CompletionData{ + NewText: "false", + Snippet: fmt.Sprintf(`${%d:false}`, nextPlaceholder), + LastPlaceholder: nextPlaceholder, + } + case cty.Number: + return CompletionData{ + NewText: "1", + Snippet: fmt.Sprintf(`${%d:1}`, nextPlaceholder), + LastPlaceholder: nextPlaceholder, + } + case cty.DynamicPseudoType: + return CompletionData{ + NewText: "", + Snippet: fmt.Sprintf(`${%d}`, nextPlaceholder), + LastPlaceholder: nextPlaceholder, + } + } + + // complex types typically span multiple lines, so indenting matters + indent := strings.Repeat(" ", lt.nestingLvl+1) + endBraceIndent := strings.Repeat(" ", lt.nestingLvl) + + if lt.Type.IsMapType() { + elCons := LiteralType{ + Type: *lt.Type.MapElementType(), + nestingLvl: lt.nestingLvl + 1, + } + elCd := elCons.EmptyCompletionData(nextPlaceholder + 1) + + return CompletionData{ + NewText: fmt.Sprintf("{\n"+`%s"key" = %s`+"%s\n}", + indent, elCd.NewText, endBraceIndent), + Snippet: fmt.Sprintf("{\n"+`%s"${%d:key}" = %s`+"%s\n}", + indent, nextPlaceholder, elCd.Snippet, endBraceIndent), + LastPlaceholder: elCd.LastPlaceholder, + } + } + + if lt.Type.IsObjectType() { + newText := "" + snippet := "" + attrNames := sortedObjectAttrNames(lt.Type) + + lastPlaceholder := nextPlaceholder + for i, name := range attrNames { + elType := lt.Type.AttributeType(name) + attrCons := LiteralType{ + Type: elType, + nestingLvl: lt.nestingLvl + 1, + } + if i > 0 { + lastPlaceholder++ + } + attrCd := attrCons.EmptyCompletionData(lastPlaceholder) + + newText += fmt.Sprintf("%s%s = %s\n", indent, name, attrCd.NewText) + snippet += fmt.Sprintf("%s%s = %s\n", indent, name, attrCd.Snippet) + lastPlaceholder = attrCd.LastPlaceholder + } + + return CompletionData{ + NewText: fmt.Sprintf("{\n%s%s}", newText, endBraceIndent), + Snippet: fmt.Sprintf("{\n%s%s}", snippet, endBraceIndent), + LastPlaceholder: lastPlaceholder, + } + } + + if lt.Type.IsListType() || lt.Type.IsSetType() { + elCons := LiteralType{ + Type: lt.Type.ElementType(), + } + elCd := elCons.EmptyCompletionData(nextPlaceholder) + + return CompletionData{ + NewText: fmt.Sprintf("[ %s ]", elCd.NewText), + Snippet: fmt.Sprintf(`[ %s ]`, elCd.Snippet), + LastPlaceholder: elCd.LastPlaceholder, + } + } + + if lt.Type.IsTupleType() { + elTypes := lt.Type.TupleElementTypes() + if len(elTypes) == 1 { + elCons := LiteralType{ + Type: elTypes[0], + } + elCd := elCons.EmptyCompletionData(nextPlaceholder) + + return CompletionData{ + NewText: fmt.Sprintf("[ %s ]", elCd.NewText), + Snippet: fmt.Sprintf(`[ %s ]`, elCd.Snippet), + LastPlaceholder: elCd.LastPlaceholder, + } + } + + newText := "" + snippet := "" + lastPlaceholder := nextPlaceholder + for i, elType := range elTypes { + elCons := LiteralType{ + Type: elType, + nestingLvl: lt.nestingLvl + 1, + } + elCd := elCons.EmptyCompletionData(lastPlaceholder + i) + + newText += elCd.NewText + ",\n" + snippet += elCd.Snippet + ",\n" + lastPlaceholder = elCd.LastPlaceholder + } + return CompletionData{ + NewText: fmt.Sprintf("[\n%s%s]", newText, endBraceIndent), + Snippet: fmt.Sprintf("[\n%s%s]", snippet, endBraceIndent), + LastPlaceholder: lastPlaceholder, + } + } + + return CompletionData{ + NewText: "", + Snippet: "", + LastPlaceholder: nextPlaceholder, + } +} + +func sortedObjectAttrNames(obj cty.Type) []string { + if !obj.IsObjectType() { + return []string{} + } + + types := obj.AttributeTypes() + names := make([]string, len(types)) + i := 0 + for name := range types { + names[i] = name + i++ + } + + sort.Strings(names) + return names }