From fb1fc7a1e575c5d1810f588a31469d1935b48fd5 Mon Sep 17 00:00:00 2001 From: Radek Simko Date: Wed, 18 Jan 2023 12:35:16 +0200 Subject: [PATCH] decoder: Implement completion for LiteralType --- decoder/expr_literal_type.go | 585 +++++++++++++++- decoder/expr_literal_type_test.go | 1086 +++++++++++++++++++++++++++++ decoder/expression.go | 5 +- schema/constraint.go | 1 + schema/constraint_literal_type.go | 38 + 5 files changed, 1712 insertions(+), 3 deletions(-) create mode 100644 decoder/expr_literal_type_test.go diff --git a/decoder/expr_literal_type.go b/decoder/expr_literal_type.go index a82e2714..5f13e3b8 100644 --- a/decoder/expr_literal_type.go +++ b/decoder/expr_literal_type.go @@ -1,24 +1,607 @@ package decoder import ( + "bytes" "context" + "fmt" + "strings" + "unicode/utf8" "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 LiteralType struct { expr hcl.Expression cons schema.LiteralType + + fileBytes []byte } func (lt LiteralType) CompletionAtPos(ctx context.Context, pos hcl.Pos) []lang.Candidate { - // TODO + 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 { + 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{} + } + + if typ.IsListType() || typ.IsSetType() { + expr, ok := lt.expr.(*hclsyntax.TupleConsExpr) + if !ok { + return []lang.Candidate{} + } + + betweenBraces := hcl.Range{ + Filename: expr.Range().Filename, + Start: expr.OpenRange.End, + End: expr.Range().End, + } + + if betweenBraces.ContainsPos(pos) { + elemCons := LiteralType{ + cons: schema.LiteralType{ + Type: typ.ElementType(), + }, + fileBytes: lt.fileBytes, + } + if len(expr.Exprs) == 0 { + elemCons.expr = newEmptyExpressionAtPos(expr.Range().Filename, pos) + return elemCons.CompletionAtPos(ctx, pos) + } + + var lastElemEndPos hcl.Pos + for _, elemExpr := range expr.Exprs { + if elemExpr.Range().ContainsPos(pos) || elemExpr.Range().End.Byte == pos.Byte { + elemCons.expr = elemExpr + return elemCons.CompletionAtPos(ctx, pos) + } + lastElemEndPos = elemExpr.Range().End + } + + rng := hcl.Range{ + Filename: expr.Range().Filename, + Start: lastElemEndPos, + End: pos, + } + b := rng.SliceBytes(lt.fileBytes) + if strings.TrimSpace(string(b)) != "," { + return []lang.Candidate{} + } + + elemCons.expr = newEmptyExpressionAtPos(expr.Range().Filename, pos) + return elemCons.CompletionAtPos(ctx, pos) + } + } + + if typ.IsTupleType() { + expr, ok := lt.expr.(*hclsyntax.TupleConsExpr) + if !ok { + return []lang.Candidate{} + } + + betweenBraces := hcl.Range{ + Filename: expr.Range().Filename, + Start: expr.OpenRange.End, + End: expr.Range().End, + } + + if betweenBraces.ContainsPos(pos) { + if len(expr.Exprs) == 0 { + elemCons := LiteralType{ + expr: newEmptyExpressionAtPos(expr.Range().Filename, pos), + cons: schema.LiteralType{ + Type: typ.TupleElementType(0), + }, + fileBytes: lt.fileBytes, + } + return elemCons.CompletionAtPos(ctx, pos) + } + + if len(expr.Exprs) <= len(typ.TupleElementTypes()) { + var lastElemEndPos hcl.Pos + // check for completion inside individual elements + for i, elemExpr := range expr.Exprs { + if elemExpr.Range().ContainsPos(pos) { + elemCons := LiteralType{ + expr: elemExpr, + cons: schema.LiteralType{ + Type: typ.TupleElementType(i), + }, + fileBytes: lt.fileBytes, + } + return elemCons.CompletionAtPos(ctx, pos) + } + lastElemEndPos = elemExpr.Range().End + } + + if pos.Byte > lastElemEndPos.Byte { + if len(expr.Exprs) == len(typ.TupleElementTypes()) { + // no more elements to complete, all declared + return []lang.Candidate{} + } + + rng := hcl.Range{ + Filename: expr.Range().Filename, + Start: lastElemEndPos, + End: pos, + } + b := rng.SliceBytes(lt.fileBytes) + if strings.TrimSpace(string(b)) != "," { + return []lang.Candidate{} + } + + nextIdx := len(expr.Exprs) + elemCons := LiteralType{ + expr: newEmptyExpressionAtPos(expr.Range().Filename, pos), + cons: schema.LiteralType{ + Type: typ.TupleElementType(nextIdx), + }, + fileBytes: lt.fileBytes, + } + return elemCons.CompletionAtPos(ctx, pos) + } + } + } + } + + if typ.IsMapType() { + expr, ok := lt.expr.(*hclsyntax.ObjectConsExpr) + if !ok { + return []lang.Candidate{} + } + + betweenBraces := hcl.Range{ + Filename: expr.Range().Filename, + Start: expr.OpenRange.End, + End: hcl.Pos{ + // exclude the trailing brace } from range + // to make byte slice comparison easier + Line: expr.Range().End.Line, + Column: expr.Range().End.Column - 1, + Byte: expr.Range().End.Byte - 1, + }, + } + + if betweenBraces.ContainsPos(pos) { + // TODO: Avoid prefilling value & TriggerSuggest for bool? + mapItemCandidate := lang.Candidate{ + Label: fmt.Sprintf("key = %s", typ.ElementType().FriendlyNameForConstraint()), + Detail: typ.ElementType().FriendlyNameForConstraint(), + Kind: candidateKindForType(typ.ElementType()), + TextEdit: lang.TextEdit{ + NewText: fmt.Sprintf("\"key\" = %s", newTextForLiteralType(typ.ElementType())), + Snippet: fmt.Sprintf("\"${1:key}\" = %s", snippetForLiteralType(2, typ.ElementType())), + Range: hcl.Range{ + Filename: lt.expr.Range().Filename, + Start: pos, + End: pos, + }, + }, + } + + slicedBytes := bytes.TrimSpace(betweenBraces.SliceBytes(lt.fileBytes)) + + if len(expr.Items) == 0 && len(slicedBytes) == 0 { + return []lang.Candidate{mapItemCandidate} + } + + var lastItemEndPos *hcl.Pos + for _, item := range expr.Items { + if item.KeyExpr.Range().ContainsPos(pos) || item.KeyExpr.Range().End.Byte == pos.Byte { + // no completion for map keys + return []lang.Candidate{} + } + if item.ValueExpr.Range().ContainsPos(pos) || item.ValueExpr.Range().End.Byte == pos.Byte { + elemCons := LiteralType{ + expr: item.ValueExpr, + cons: schema.LiteralType{ + Type: typ.ElementType(), + }, + fileBytes: lt.fileBytes, + } + + return elemCons.CompletionAtPos(ctx, pos) + } + // only consider items declared before position + if pos.Byte > item.ValueExpr.Range().End.Byte { + vep := item.ValueExpr.Range().End + lastItemEndPos = &vep + } + } + + if lastItemEndPos != nil { + if pos.Line == lastItemEndPos.Line && pos.Column < lastItemEndPos.Column { + // reject completion inside indentation space + return []lang.Candidate{} + } + + // completing new item on new line + if pos.Line > lastItemEndPos.Line { + lineBytes := bytes.TrimSpace(sliceBytesOnPosLine(lt.fileBytes, pos)) + if len(lineBytes) == 0 { + return []lang.Candidate{mapItemCandidate} + } + if lineBytes[len(lineBytes)-1] == '=' { + elemCons := LiteralType{ + expr: newEmptyExpressionAtPos(expr.Range().Filename, pos), + cons: schema.LiteralType{ + Type: typ.ElementType(), + }, + fileBytes: lt.fileBytes, + } + + return elemCons.CompletionAtPos(ctx, pos) + } + + return []lang.Candidate{} + } + } + + // completing empty map + if len(slicedBytes) == 0 { + return []lang.Candidate{ + mapItemCandidate, + } + } + + if slicedBytes[len(slicedBytes)-1] == '=' { + // completing value of a new item + elemCons := LiteralType{ + expr: newEmptyExpressionAtPos(expr.Range().Filename, pos), + cons: schema.LiteralType{ + Type: typ.ElementType(), + }, + fileBytes: lt.fileBytes, + } + + return elemCons.CompletionAtPos(ctx, pos) + } + + return []lang.Candidate{} + } + + } + + if typ.IsObjectType() { + expr, ok := lt.expr.(*hclsyntax.ObjectConsExpr) + if !ok { + return []lang.Candidate{} + } + + betweenBraces := hcl.Range{ + Filename: expr.Range().Filename, + Start: expr.OpenRange.End, + End: hcl.Pos{ + // exclude the trailing brace } from range + // to make byte slice comparison easier + Line: expr.Range().End.Line, + Column: expr.Range().End.Column - 1, + Byte: expr.Range().End.Byte - 1, + }, + } + + if betweenBraces.ContainsPos(pos) { + slicedBytes := bytes.TrimSpace(betweenBraces.SliceBytes(lt.fileBytes)) + + if len(expr.Items) == 0 && len(slicedBytes) == 0 { + return literalObjectTypesToCandidates("", typ, map[string]struct{}{}, hcl.Range{ + Filename: expr.Range().Filename, + Start: pos, + End: pos, + }) + } + + var lastItemEndPos *hcl.Pos + declaredAttributes := make(map[string]struct{}, 0) + for _, item := range expr.Items { + // only consider items declared before position + // to enable completion in between items + if pos.Byte > item.ValueExpr.Range().End.Byte { + vep := item.ValueExpr.Range().End + lastItemEndPos = &vep + } + + attrName, attrRange, ok := getRawLiteralAttributeName(item.KeyExpr) + if !ok { + continue + } + + declaredAttributes[attrName] = struct{}{} + + if item.KeyExpr.Range().ContainsPos(pos) || item.KeyExpr.Range().End.Byte == pos.Byte { + prefixLen := pos.Byte - attrRange.Start.Byte + prefix := attrName[0:prefixLen] + + editRange := hcl.RangeBetween(item.KeyExpr.Range(), item.ValueExpr.Range()) + + return literalObjectTypesToCandidates(prefix, typ, map[string]struct{}{}, editRange) + } + + if item.ValueExpr.Range().ContainsPos(pos) || item.ValueExpr.Range().End.Byte == pos.Byte { + if !typ.HasAttribute(attrName) { + // no completion for unknown attribute + return []lang.Candidate{} + } + + elemCons := LiteralType{ + expr: item.ValueExpr, + cons: schema.LiteralType{ + Type: typ.AttributeType(attrName), + }, + fileBytes: lt.fileBytes, + } + + return elemCons.CompletionAtPos(ctx, pos) + } + } + + if lastItemEndPos != nil { + if pos.Line == lastItemEndPos.Line && pos.Column < lastItemEndPos.Column { + // reject completion inside indentation space + return []lang.Candidate{} + } + if pos.Line > lastItemEndPos.Line { + lineBytes := bytes.TrimSpace(sliceBytesOnPosLine(lt.fileBytes, pos)) + + if len(lineBytes) == 0 { + return literalObjectTypesToCandidates("", typ, declaredAttributes, hcl.Range{ + Filename: expr.Range().Filename, + Start: pos, + End: pos, + }) + } + + if lineBytes[len(lineBytes)-1] == '=' { + attrName := string(bytes.TrimSpace(lineBytes[:len(lineBytes)-1])) + attrType, ok := typ.AttributeTypes()[attrName] + if !ok { + // unknown attribute + return []lang.Candidate{} + } + + elemCons := LiteralType{ + expr: newEmptyExpressionAtPos(expr.Range().Filename, pos), + cons: schema.LiteralType{ + Type: attrType, + }, + fileBytes: lt.fileBytes, + } + + return elemCons.CompletionAtPos(ctx, pos) + } + + prefix := string(lineBytes) + return literalObjectTypesToCandidates(prefix, typ, declaredAttributes, hcl.Range{ + Filename: expr.Range().Filename, + Start: hcl.Pos{ + Line: pos.Line, + Column: pos.Column - len(prefix), + Byte: pos.Byte - len(prefix), + }, + End: pos, + }) + } + } + + // completing empty object + if len(slicedBytes) == 0 { + return literalObjectTypesToCandidates("", typ, map[string]struct{}{}, hcl.Range{ + Filename: expr.Range().Filename, + Start: pos, + End: pos, + }) + } + + if slicedBytes[len(slicedBytes)-1] == '=' { + attrName := string(bytes.TrimSpace(slicedBytes[:len(slicedBytes)-1])) + attrType, ok := typ.AttributeTypes()[attrName] + if !ok { + return []lang.Candidate{} + } + + // completing value of a new item + elemCons := LiteralType{ + expr: newEmptyExpressionAtPos(expr.Range().Filename, pos), + cons: schema.LiteralType{ + Type: attrType, + }, + fileBytes: lt.fileBytes, + } + + return elemCons.CompletionAtPos(ctx, pos) + } + + prefix := string(slicedBytes) + return literalObjectTypesToCandidates(prefix, typ, declaredAttributes, hcl.Range{ + Filename: expr.Range().Filename, + Start: hcl.Pos{ + Line: pos.Line, + Column: pos.Column - len(prefix), + Byte: pos.Byte - len(prefix), + }, + End: pos, + }) + } + } + return nil } +func sliceBytesOnPosLine(b []byte, pos hcl.Pos) []byte { + for offset := pos.Byte - 1; offset >= 0; offset-- { + nextRune, _ := utf8.DecodeRune(b[offset:]) + if nextRune == '\n' { + return b[offset:pos.Byte] + } + } + return []byte{} +} + +func getRawLiteralAttributeName(keyExpr hclsyntax.Expression) (string, *hcl.Range, bool) { + switch eType := keyExpr.(type) { + case *hclsyntax.ScopeTraversalExpr: + if len(eType.Traversal) != 1 { + return "", nil, false + } + return eType.Traversal.RootName(), eType.Range().Ptr(), true + + // account for quoted keys which are not best practice but allowed + case *hclsyntax.LiteralValueExpr: + if eType.Val.Type() != cty.String { + return "", nil, false + } + + attrName := eType.Val.AsString() + + return attrName, &hcl.Range{ + Filename: eType.Range().Filename, + // account for enclosing quotes + Start: hcl.Pos{ + Line: eType.Range().Start.Line, + Column: eType.Range().Start.Column - 1, + Byte: eType.Range().Start.Byte - 1, + }, + End: hcl.Pos{ + Line: eType.Range().End.Line, + Column: eType.Range().End.Column - 1, + Byte: eType.Range().End.Byte - 1, + }, + }, true + + case *hclsyntax.ObjectConsKeyExpr: + return getRawLiteralAttributeName(eType.Wrapped) + } + + return "", nil, false +} + +func literalObjectTypesToCandidates(prefix string, objType cty.Type, declaredAttrs map[string]struct{}, editRange hcl.Range) []lang.Candidate { + attrTypes := objType.AttributeTypes() + if len(attrTypes) == 0 { + return []lang.Candidate{} + } + + candidates := make([]lang.Candidate, 0) + + attrNames := sortedObjectAttrNames(objType) + + for _, name := range attrNames { + if !strings.HasPrefix(name, prefix) { + continue + } + if _, ok := declaredAttrs[name]; ok { + continue + } + + attrType := objType.AttributeType(name) + detail := attrType.FriendlyNameForConstraint() + if objType.AttributeOptional(name) { + detail += ", optional" + } else { + detail += ", required" + } + + // TODO: Avoid prefilling value & TriggerSuggest for bool? + candidate := lang.Candidate{ + Label: name, + Detail: detail, + Kind: candidateKindForType(attrType), + TextEdit: lang.TextEdit{ + NewText: fmt.Sprintf("%s = %s", name, newTextForLiteralType(attrType)), + Snippet: fmt.Sprintf("%s = %s", name, snippetForLiteralType(1, attrType)), + Range: editRange, + }, + } + + candidates = append(candidates, candidate) + } + + return candidates +} + +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 (lt LiteralType) HoverAtPos(ctx context.Context, pos hcl.Pos) *lang.HoverData { // TODO return nil diff --git a/decoder/expr_literal_type_test.go b/decoder/expr_literal_type_test.go new file mode 100644 index 00000000..1c47cfa8 --- /dev/null +++ b/decoder/expr_literal_type_test.go @@ -0,0 +1,1086 @@ +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_exprTypeDeclaration(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.BoolCandidateKind, + 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.BoolCandidateKind, + 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.BoolCandidateKind, + 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{}), + }, + { + "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: "bool, required", + Kind: lang.BoolCandidateKind, + TextEdit: lang.TextEdit{ + NewText: "bar = false", + 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: "number, required", + Kind: lang.NumberCandidateKind, + TextEdit: lang.TextEdit{ + NewText: "baz = 1", + Snippet: "baz = ${1:1}", + 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: "string, required", + Kind: lang.StringCandidateKind, + 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: "bool, required", + Kind: lang.BoolCandidateKind, + TextEdit: lang.TextEdit{ + NewText: "bar = false", + 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: "bool, required", + Kind: lang.BoolCandidateKind, + TextEdit: lang.TextEdit{ + NewText: "bar = false", + 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: "bool, required", + Kind: lang.BoolCandidateKind, + TextEdit: lang.TextEdit{ + NewText: "bar = false", + 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: "number, required", + Kind: lang.NumberCandidateKind, + TextEdit: lang.TextEdit{ + NewText: "baz = 1", + Snippet: "baz = ${1:1}", + 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: "bool, required", + Kind: lang.BoolCandidateKind, + TextEdit: lang.TextEdit{ + NewText: "bar = false", + 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: "number, required", + Kind: lang.NumberCandidateKind, + TextEdit: lang.TextEdit{ + NewText: "baz = 1", + Snippet: "baz = ${1:1}", + 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 8ab859cd..512f5d91 100644 --- a/decoder/expression.go +++ b/decoder/expression.go @@ -35,8 +35,9 @@ func newExpression(pathContext *PathContext, expr hcl.Expression, cons schema.Co } case schema.LiteralType: return LiteralType{ - expr: expr, - cons: c, + expr: expr, + cons: c, + fileBytes: pathContext.Files[expr.Range().Filename].Bytes, } case schema.LiteralValue: return LiteralValue{ diff --git a/schema/constraint.go b/schema/constraint.go index cf97f6d6..a4267046 100644 --- a/schema/constraint.go +++ b/schema/constraint.go @@ -29,6 +29,7 @@ type Comparable interface { } type CompletionData struct { + Label string NewText string Snippet string TriggerSuggest bool diff --git a/schema/constraint_literal_type.go b/schema/constraint_literal_type.go index 0680a506..51b551ed 100644 --- a/schema/constraint_literal_type.go +++ b/schema/constraint_literal_type.go @@ -46,24 +46,28 @@ func (lt LiteralType) EmptyCompletionData(nextPlaceholder int) CompletionData { switch lt.Type { case cty.String: return CompletionData{ + Label: `""`, NewText: `""`, Snippet: fmt.Sprintf(`"${%d:value}"`, nextPlaceholder), LastPlaceholder: nextPlaceholder, } case cty.Bool: return CompletionData{ + Label: `false`, NewText: "false", Snippet: fmt.Sprintf(`${%d:false}`, nextPlaceholder), LastPlaceholder: nextPlaceholder, } case cty.Number: return CompletionData{ + Label: `1`, NewText: "1", Snippet: fmt.Sprintf(`${%d:1}`, nextPlaceholder), LastPlaceholder: nextPlaceholder, } case cty.DynamicPseudoType: return CompletionData{ + Label: "", NewText: "", Snippet: fmt.Sprintf(`${%d}`, nextPlaceholder), LastPlaceholder: nextPlaceholder, @@ -82,6 +86,7 @@ func (lt LiteralType) EmptyCompletionData(nextPlaceholder int) CompletionData { elCd := elCons.EmptyCompletionData(nextPlaceholder + 1) return CompletionData{ + Label: fmt.Sprintf(`{ "key" = %s }`, elCd.NewText), NewText: fmt.Sprintf("{\n"+`%s"key" = %s`+"%s\n}", indent, elCd.NewText, endBraceIndent), Snippet: fmt.Sprintf("{\n"+`%s"${%d:key}" = %s`+"%s\n}", @@ -112,7 +117,25 @@ func (lt LiteralType) EmptyCompletionData(nextPlaceholder int) CompletionData { lastPlaceholder = attrCd.LastPlaceholder } + label := "{ }" + if len(attrNames) > 0 { + attrName := attrNames[0] + elType := lt.Type.AttributeType(attrName) + attrCons := LiteralType{ + Type: elType, + nestingLvl: lt.nestingLvl, + } + cData := attrCons.EmptyCompletionData(lastPlaceholder) + + label = fmt.Sprintf(`{ %s = %s`, attrName, cData.Label) + if len(attrNames) > 1 { + label = `, …` + } + label += " }" + } + return CompletionData{ + Label: label, NewText: fmt.Sprintf("{\n%s%s}", newText, endBraceIndent), Snippet: fmt.Sprintf("{\n%s%s}", snippet, endBraceIndent), LastPlaceholder: lastPlaceholder, @@ -126,6 +149,7 @@ func (lt LiteralType) EmptyCompletionData(nextPlaceholder int) CompletionData { elCd := elCons.EmptyCompletionData(nextPlaceholder) return CompletionData{ + Label: fmt.Sprintf("[ %s ]", elCd.Label), NewText: fmt.Sprintf("[ %s ]", elCd.NewText), Snippet: fmt.Sprintf(`[ %s ]`, elCd.Snippet), LastPlaceholder: elCd.LastPlaceholder, @@ -141,12 +165,14 @@ func (lt LiteralType) EmptyCompletionData(nextPlaceholder int) CompletionData { elCd := elCons.EmptyCompletionData(nextPlaceholder) return CompletionData{ + Label: fmt.Sprintf("[ %s ]", elCd.Label), NewText: fmt.Sprintf("[ %s ]", elCd.NewText), Snippet: fmt.Sprintf(`[ %s ]`, elCd.Snippet), LastPlaceholder: elCd.LastPlaceholder, } } + label := "[ " newText := "" snippet := "" lastPlaceholder := nextPlaceholder @@ -157,11 +183,22 @@ func (lt LiteralType) EmptyCompletionData(nextPlaceholder int) CompletionData { } elCd := elCons.EmptyCompletionData(lastPlaceholder + i) + if i == 0 { + label += elCd.Label + } else if len(label) < 10 { + label += fmt.Sprintf(", %s", elCd.Label) + } else { + label += ", …" + } + newText += elCd.NewText + ",\n" snippet += elCd.Snippet + ",\n" lastPlaceholder = elCd.LastPlaceholder } + label += " ]" + return CompletionData{ + Label: label, NewText: fmt.Sprintf("[\n%s%s]", newText, endBraceIndent), Snippet: fmt.Sprintf("[\n%s%s]", snippet, endBraceIndent), LastPlaceholder: lastPlaceholder, @@ -169,6 +206,7 @@ func (lt LiteralType) EmptyCompletionData(nextPlaceholder int) CompletionData { } return CompletionData{ + Label: "", NewText: "", Snippet: "", LastPlaceholder: nextPlaceholder,