diff --git a/decoder/expr_object.go b/decoder/expr_object.go index 1ca5b1bb..1085e8b3 100644 --- a/decoder/expr_object.go +++ b/decoder/expr_object.go @@ -7,6 +7,8 @@ import ( "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 Object struct { @@ -15,11 +17,6 @@ type Object struct { pathCtx *PathContext } -func (obj Object) CompletionAtPos(ctx context.Context, pos hcl.Pos) []lang.Candidate { - // TODO - return nil -} - func (obj Object) HoverAtPos(ctx context.Context, pos hcl.Pos) *lang.HoverData { // TODO return nil @@ -40,32 +37,31 @@ func (obj Object) ReferenceTargets(ctx context.Context, addr lang.Address, addrC return nil } -type ObjectAttributes struct { - expr hcl.Expression - cons schema.ObjectAttributes -} +func getRawObjectAttributeName(keyExpr hcl.Expression) (string, *hcl.Range, bool) { + switch eType := keyExpr.(type) { + // regardless of what expression it is always wrapped + case *hclsyntax.ObjectConsKeyExpr: + return getRawObjectAttributeName(eType.Wrapped) -func (oa ObjectAttributes) CompletionAtPos(ctx context.Context, pos hcl.Pos) []lang.Candidate { - // TODO - return nil -} + // most common "naked" keys + case *hclsyntax.ScopeTraversalExpr: + if len(eType.Traversal) != 1 { + return "", nil, false + } + return eType.Traversal.RootName(), eType.Range().Ptr(), true -func (oa ObjectAttributes) HoverAtPos(ctx context.Context, pos hcl.Pos) *lang.HoverData { - // TODO - return nil -} + // less common quoted keys + case *hclsyntax.TemplateExpr: + if !eType.IsStringLiteral() { + return "", nil, false + } + return getRawObjectAttributeName(eType.Parts[0]) + case *hclsyntax.LiteralValueExpr: + if eType.Val.Type() != cty.String { + return "", nil, false + } + return eType.Val.AsString(), eType.Range().Ptr(), true + } -func (oa ObjectAttributes) SemanticTokens(ctx context.Context) []lang.SemanticToken { - // TODO - return nil -} - -func (oa ObjectAttributes) ReferenceOrigins(ctx context.Context, allowSelfRefs bool) reference.Origins { - // TODO - return nil -} - -func (oa ObjectAttributes) ReferenceTargets(ctx context.Context, addr lang.Address, addrCtx AddressContext) reference.Targets { - // TODO - return nil + return "", nil, false } diff --git a/decoder/expr_object_completion.go b/decoder/expr_object_completion.go new file mode 100644 index 00000000..a209ed06 --- /dev/null +++ b/decoder/expr_object_completion.go @@ -0,0 +1,280 @@ +package decoder + +import ( + "bytes" + "context" + "sort" + "strings" + "unicode" + + "github.com/hashicorp/hcl-lang/lang" + "github.com/hashicorp/hcl-lang/schema" + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hclsyntax" +) + +type declaredAttributes map[string]hcl.Range + +func (obj Object) CompletionAtPos(ctx context.Context, pos hcl.Pos) []lang.Candidate { + if isEmptyExpression(obj.expr) { + return []lang.Candidate{ + { // TODO: Consider rendering first N elements in Label? + Label: "{…}", + Detail: "object", + Kind: lang.ObjectCandidateKind, + Description: obj.cons.Description, + TextEdit: lang.TextEdit{ + NewText: "{\n \n}", + Snippet: "{\n ${0}\n}", + Range: hcl.Range{ + Filename: obj.expr.Range().Filename, + Start: pos, + End: pos, + }, + }, + TriggerSuggest: true, + }, + } + } + + eType, ok := obj.expr.(*hclsyntax.ObjectConsExpr) + if !ok { + return []lang.Candidate{} + } + + betweenBraces := hcl.Range{ + Filename: eType.Range().Filename, + Start: eType.OpenRange.End, + End: eType.Range().End, + } + + if !betweenBraces.ContainsPos(pos) { + return []lang.Candidate{} + } + + if len(obj.cons.Attributes) == 0 { + return []lang.Candidate{} + } + + editRange := hcl.Range{ + Filename: eType.Range().Filename, + Start: pos, + End: pos, + } + + declared := make(declaredAttributes, 0) + recoveryPos := eType.OpenRange.Start + var lastItemRange, nextItemRange *hcl.Range + + for _, item := range eType.Items { + emptyRange := hcl.Range{ + Filename: eType.Range().Filename, + Start: item.KeyExpr.Range().End, + End: item.ValueExpr.Range().Start, + } + if emptyRange.ContainsPos(pos) { + // exit early if we're in empty space between key and value + return []lang.Candidate{} + } + + attrName, attrRange, ok := getRawObjectAttributeName(item.KeyExpr) + if !ok { + continue + } + + // collect all declared attributes + declared[attrName] = hcl.RangeBetween(item.KeyExpr.Range(), item.ValueExpr.Range()) + + if nextItemRange != nil { + continue + } + // check if we've just missed the position + if pos.Byte < item.KeyExpr.Range().Start.Byte { + // record current (next) item so we can avoid completion + // on the same line in multi-line mode (without comma) + nextItemRange = hcl.RangeBetween(item.KeyExpr.Range(), item.ValueExpr.Range()).Ptr() + + // enable recovery of incomplete configuration + // between last item's end and position + continue + } + lastItemRange = hcl.RangeBetween(item.KeyExpr.Range(), item.ValueExpr.Range()).Ptr() + recoveryPos = item.ValueExpr.Range().End + + if item.KeyExpr.Range().ContainsPos(pos) { + prefix := "" + + // if we're before start of the attribute + // it means the attribute is likely quoted + if pos.Byte >= attrRange.Start.Byte { + prefixLen := pos.Byte - attrRange.Start.Byte + prefix = attrName[0:prefixLen] + } + + editRange := hcl.RangeBetween(item.KeyExpr.Range(), item.ValueExpr.Range()) + + return objectAttributesToCandidates(prefix, obj.cons.Attributes, declared, editRange) + } + if item.ValueExpr.Range().ContainsPos(pos) || item.ValueExpr.Range().End.Byte == pos.Byte { + aSchema, ok := obj.cons.Attributes[attrName] + if !ok { + // unknown attribute + return []lang.Candidate{} + } + + cons := newExpression(obj.pathCtx, item.ValueExpr, aSchema.Constraint) + + return cons.CompletionAtPos(ctx, pos) + } + } + + // check any incomplete configuration up to a terminating character + fileBytes := obj.pathCtx.Files[eType.Range().Filename].Bytes + leftBytes := recoverMatchingLeftBytes(fileBytes, pos, func(offset int, r rune) bool { + return !isObjectItemLeftTerminator(r) && offset > recoveryPos.Byte + }) + trimmedBytes := bytes.TrimRight(leftBytes, " \t") + + if len(trimmedBytes) == 0 { + // no terminating character was found which indicates + // we're on the same line as an existing item + // and we're missing preceding comma + return []lang.Candidate{} + } + + if len(trimmedBytes) == 1 && isObjectItemLeftTerminator(rune(trimmedBytes[0])) { + // avoid completing on the same line as next item + if nextItemRange != nil && nextItemRange.Start.Line == pos.Line { + return []lang.Candidate{} + } + + // avoid completing on the same line as last item + if lastItemRange != nil && lastItemRange.End.Line == pos.Line { + // if it is not single-line notation + if trimmedBytes[0] != ',' { + return []lang.Candidate{} + } + } + + return objectAttributesToCandidates("", obj.cons.Attributes, declared, editRange) + } + + // trime left side as well now + // to make prefix/attribute extraction easier below + trimmedBytes = bytes.TrimLeftFunc(trimmedBytes, func(r rune) bool { + return isObjectItemLeftTerminator(r) || unicode.IsSpace(r) + }) + + // if last byte is =, then it's incomplete attribute + if trimmedBytes[len(trimmedBytes)-1] == '=' { + emptyExpr := newEmptyExpressionAtPos(eType.Range().Filename, pos) + + attrName := string(bytes.TrimFunc(trimmedBytes[:len(trimmedBytes)-1], func(r rune) bool { + return unicode.IsSpace(r) || r == '"' + })) + aSchema, ok := obj.cons.Attributes[attrName] + if !ok { + // unknown attribute + return []lang.Candidate{} + } + + cons := newExpression(obj.pathCtx, emptyExpr, aSchema.Constraint) + + return cons.CompletionAtPos(ctx, pos) + } + + prefix := string(bytes.TrimFunc(trimmedBytes, func(r rune) bool { + return unicode.IsSpace(r) || r == '"' + })) + + // calculate appropriate edit range in case there + // are also characters on the right from position + // which are worth replacing + remainingRange := hcl.Range{ + Filename: eType.Range().Filename, + Start: pos, + End: eType.SrcRange.End, + } + editRange = objectItemPrefixBasedEditRange(remainingRange, fileBytes, trimmedBytes) + + return objectAttributesToCandidates(prefix, obj.cons.Attributes, declared, editRange) +} + +func isObjectItemLeftTerminator(r rune) bool { + return r == '\n' || r == ',' || r == '{' +} + +func recoverMatchingLeftBytes(b []byte, pos hcl.Pos, f func(byteOffset int, r rune) bool) []byte { + for offset := pos.Byte - 1; offset >= 0; offset-- { + nextRune := rune(b[offset]) + if !f(offset, nextRune) { + return b[offset:pos.Byte] + } + } + return []byte{} +} + +func objectItemPrefixBasedEditRange(remainingRange hcl.Range, fileBytes []byte, rawPrefixBytes []byte) hcl.Range { + remainingBytes := remainingRange.SliceBytes(fileBytes) + roughEndByteOffset := bytes.IndexFunc(remainingBytes, func(r rune) bool { + return r == '\n' || r == '}' + }) + // avoid editing over whitespace + trimmedRightBytes := bytes.TrimRightFunc(remainingBytes[:roughEndByteOffset], func(r rune) bool { + return unicode.IsSpace(r) + }) + trimmedOffset := len(trimmedRightBytes) + + return hcl.Range{ + Filename: remainingRange.Filename, + Start: hcl.Pos{ + // TODO: Calculate Line+Column for multi-line keys? + Line: remainingRange.Start.Line, + Column: remainingRange.Start.Column - len(rawPrefixBytes), + Byte: remainingRange.Start.Byte - len(rawPrefixBytes), + }, + End: hcl.Pos{ + // TODO: Calculate Line+Column for multi-line values? + Line: remainingRange.Start.Line, + Column: remainingRange.Start.Column + trimmedOffset, + Byte: remainingRange.Start.Byte + trimmedOffset, + }, + } +} + +func objectAttributesToCandidates(prefix string, attrs schema.ObjectAttributes, declared declaredAttributes, editRange hcl.Range) []lang.Candidate { + if len(attrs) == 0 { + return []lang.Candidate{} + } + + candidates := make([]lang.Candidate, 0) + + attrNames := sortedObjectAttributeNames(attrs) + + for _, name := range attrNames { + if !strings.HasPrefix(name, prefix) { + continue + } + // avoid suggesting already declared attribute + // unless we're overriding it + if declaredRng, ok := declared[name]; ok && !declaredRng.Overlaps(editRange) { + continue + } + + candidates = append(candidates, attributeSchemaToCandidate(name, attrs[name], editRange)) + } + + return candidates +} + +func sortedObjectAttributeNames(objAttributes schema.ObjectAttributes) []string { + names := make([]string, len(objAttributes)) + i := 0 + for name := range objAttributes { + names[i] = name + i++ + } + + sort.Strings(names) + return names +} diff --git a/decoder/expr_object_completion_test.go b/decoder/expr_object_completion_test.go new file mode 100644 index 00000000..55c16275 --- /dev/null +++ b/decoder/expr_object_completion_test.go @@ -0,0 +1,1744 @@ +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 TestCompletionAtPos_exprObject(t *testing.T) { + testCases := []struct { + testName string + attrSchema map[string]*schema.AttributeSchema + cfg string + pos hcl.Pos + expectedCandidates lang.Candidates + }{ + { + "empty expression no element", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.Object{}, + }, + }, + `attr = +`, + hcl.Pos{Line: 1, Column: 8, Byte: 7}, + lang.CompleteCandidates([]lang.Candidate{ + { + Label: `{…}`, + Detail: "object", + TextEdit: lang.TextEdit{ + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + End: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + }, + NewText: "{\n \n}", + Snippet: "{\n ${0}\n}", + }, + Kind: lang.ObjectCandidateKind, + TriggerSuggest: true, + }, + }), + }, + { + "empty expression with element", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.Object{ + Attributes: schema.ObjectAttributes{ + "foo": { + IsOptional: true, + Constraint: schema.Keyword{ + Keyword: "kw", + }, + }, + }, + }, + }, + }, + `attr = +`, + hcl.Pos{Line: 1, Column: 8, Byte: 7}, + lang.CompleteCandidates([]lang.Candidate{ + { + Label: `{…}`, + Detail: "object", + TextEdit: lang.TextEdit{ + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + End: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + }, + NewText: "{\n \n}", + Snippet: "{\n ${0}\n}", + }, + Kind: lang.ObjectCandidateKind, + TriggerSuggest: true, + }, + }), + }, + + // single line tests + { + "inside braces single-line", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.Object{ + Attributes: schema.ObjectAttributes{ + "foo": { + IsOptional: true, + Constraint: schema.Keyword{ + Keyword: "keyword", + }, + }, + }, + }, + }, + }, + `attr = { } +`, + hcl.Pos{Line: 1, Column: 10, Byte: 9}, + lang.CompleteCandidates([]lang.Candidate{ + { + Label: `foo`, + Detail: "optional, keyword", + TextEdit: lang.TextEdit{ + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 10, Byte: 9}, + End: hcl.Pos{Line: 1, Column: 10, Byte: 9}, + }, + NewText: `foo`, + Snippet: `foo = `, + }, + Kind: lang.AttributeCandidateKind, + TriggerSuggest: true, + }, + }), + }, + { + "single-line new element inside attribute name with no equal sign", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.Object{ + Attributes: schema.ObjectAttributes{ + "foo": { + IsOptional: true, + Constraint: schema.Keyword{ + Keyword: "kw", + }, + }, + }, + }, + }, + }, + `attr = { foo } +`, + hcl.Pos{Line: 1, Column: 11, Byte: 10}, + lang.CompleteCandidates([]lang.Candidate{ + { + Label: `foo`, + Detail: "optional, keyword", + TextEdit: lang.TextEdit{ + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 10, Byte: 9}, + End: hcl.Pos{Line: 1, Column: 13, Byte: 12}, + }, + NewText: `foo`, + Snippet: `foo = `, + }, + Kind: lang.AttributeCandidateKind, + TriggerSuggest: true, + }, + }), + }, + { + "single-line new element inside quoted attribute name with no equal sign", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.Object{ + Attributes: schema.ObjectAttributes{ + "foo": { + IsOptional: true, + Constraint: schema.Keyword{ + Keyword: "kw", + }, + }, + }, + }, + }, + }, + `attr = { "foo" } +`, + hcl.Pos{Line: 1, Column: 12, Byte: 11}, + lang.CompleteCandidates([]lang.Candidate{ + { + Label: `foo`, + Detail: "optional, keyword", + TextEdit: lang.TextEdit{ + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 10, Byte: 9}, + End: hcl.Pos{Line: 1, Column: 15, Byte: 14}, + }, + NewText: `foo`, + Snippet: `foo = `, + }, + Kind: lang.AttributeCandidateKind, + TriggerSuggest: true, + }, + }), + }, + { + "single-line new element value after equal sign", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.Object{ + Attributes: schema.ObjectAttributes{ + "foo": { + IsOptional: true, + Constraint: schema.Keyword{ + Keyword: "kw", + }, + }, + }, + }, + }, + }, + `attr = { foo = } +`, + hcl.Pos{Line: 1, Column: 16, Byte: 15}, + lang.CompleteCandidates([]lang.Candidate{ + { + Label: `kw`, + Detail: "keyword", + TextEdit: lang.TextEdit{ + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 16, Byte: 15}, + End: hcl.Pos{Line: 1, Column: 16, Byte: 15}, + }, + NewText: `kw`, + Snippet: `kw`, + }, + Kind: lang.KeywordCandidateKind, + }, + }), + }, + { + "single-line new quoted element value after equal sign", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.Object{ + Attributes: schema.ObjectAttributes{ + "foo": { + IsOptional: true, + Constraint: schema.Keyword{ + Keyword: "kw", + }, + }, + }, + }, + }, + }, + `attr = { "foo" = } +`, + hcl.Pos{Line: 1, Column: 18, Byte: 17}, + lang.CompleteCandidates([]lang.Candidate{ + { + Label: `kw`, + Detail: "keyword", + TextEdit: lang.TextEdit{ + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 18, Byte: 17}, + End: hcl.Pos{Line: 1, Column: 18, Byte: 17}, + }, + NewText: `kw`, + Snippet: `kw`, + }, + Kind: lang.KeywordCandidateKind, + }, + }), + }, + { + "single-line new element inside attribute name", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.Object{ + Attributes: schema.ObjectAttributes{ + "foo": { + IsOptional: true, + Constraint: schema.Keyword{ + Keyword: "kw", + }, + }, + }, + }, + }, + }, + `attr = { foo = } +`, + hcl.Pos{Line: 1, Column: 11, Byte: 10}, + lang.CompleteCandidates([]lang.Candidate{ + { + Label: `foo`, + Detail: "optional, keyword", + TextEdit: lang.TextEdit{ + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 10, Byte: 9}, + End: hcl.Pos{Line: 1, Column: 15, Byte: 14}, + }, + NewText: `foo`, + Snippet: `foo = `, + }, + Kind: lang.AttributeCandidateKind, + TriggerSuggest: true, + }, + }), + }, + { + "single-line new element inside quoted attribute name", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.Object{ + Attributes: schema.ObjectAttributes{ + "foo": { + IsOptional: true, + Constraint: schema.Keyword{ + Keyword: "kw", + }, + }, + }, + }, + }, + }, + `attr = { "foo" = } +`, + hcl.Pos{Line: 1, Column: 12, Byte: 11}, + lang.CompleteCandidates([]lang.Candidate{ + { + Label: `foo`, + Detail: "optional, keyword", + TextEdit: lang.TextEdit{ + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 10, Byte: 9}, + End: hcl.Pos{Line: 1, Column: 17, Byte: 16}, + }, + NewText: `foo`, + Snippet: `foo = `, + }, + Kind: lang.AttributeCandidateKind, + TriggerSuggest: true, + }, + }), + }, + { + "inside single-line object partial attribute", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.Object{ + Attributes: schema.ObjectAttributes{ + "foo": { + Constraint: schema.Keyword{Keyword: "kw1"}, + IsOptional: true, + }, + "bar": { + Constraint: schema.Keyword{Keyword: "kw2"}, + IsOptional: true, + }, + }, + }, + }, + }, + `attr = { b }`, + hcl.Pos{Line: 1, Column: 11, Byte: 10}, + lang.CompleteCandidates([]lang.Candidate{ + { + Label: "bar", + Detail: "optional, keyword", + Kind: lang.AttributeCandidateKind, + TextEdit: lang.TextEdit{ + NewText: "bar", + Snippet: "bar = ", + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 10, Byte: 9}, + End: hcl.Pos{Line: 1, Column: 11, Byte: 10}, + }, + }, + TriggerSuggest: true, + }, + }), + }, + { + "single-line new element partial value near end", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.Object{ + Attributes: schema.ObjectAttributes{ + "foo": { + IsOptional: true, + Constraint: schema.Keyword{ + Keyword: "kw", + }, + }, + }, + }, + }, + }, + `attr = { foo = k } +`, + hcl.Pos{Line: 1, Column: 17, Byte: 16}, + lang.CompleteCandidates([]lang.Candidate{ + { + Label: `kw`, + Detail: "keyword", + TextEdit: lang.TextEdit{ + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 16, Byte: 15}, + End: hcl.Pos{Line: 1, Column: 17, Byte: 16}, + }, + NewText: `kw`, + Snippet: `kw`, + }, + Kind: lang.KeywordCandidateKind, + }, + }), + }, + { + "single-line element partial value in the middle of value", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.Object{ + Attributes: schema.ObjectAttributes{ + "foo": { + IsOptional: true, + Constraint: schema.Keyword{ + Keyword: "keyword", + }, + }, + }, + }, + }, + }, + `attr = { foo = keyword } +`, + hcl.Pos{Line: 1, Column: 18, Byte: 17}, + lang.CompleteCandidates([]lang.Candidate{ + { + Label: `keyword`, + Detail: "keyword", + TextEdit: lang.TextEdit{ + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 16, Byte: 15}, + End: hcl.Pos{Line: 1, Column: 23, Byte: 22}, + }, + NewText: `keyword`, + Snippet: `keyword`, + }, + Kind: lang.KeywordCandidateKind, + }, + }), + }, + { + "single-line quoted element partial value in the middle of value", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.Object{ + Attributes: schema.ObjectAttributes{ + "foo": { + IsOptional: true, + Constraint: schema.Keyword{ + Keyword: "keyword", + }, + }, + }, + }, + }, + }, + `attr = { "foo" = keyword } +`, + hcl.Pos{Line: 1, Column: 21, Byte: 20}, + lang.CompleteCandidates([]lang.Candidate{ + { + Label: `keyword`, + Detail: "keyword", + TextEdit: lang.TextEdit{ + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 18, Byte: 17}, + End: hcl.Pos{Line: 1, Column: 25, Byte: 24}, + }, + NewText: `keyword`, + Snippet: `keyword`, + }, + Kind: lang.KeywordCandidateKind, + }, + }), + }, + { + "single-line element partial value in the middle of attribute name", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.Object{ + Attributes: schema.ObjectAttributes{ + "foo": { + IsOptional: true, + Constraint: schema.Keyword{ + Keyword: "keyword", + }, + }, + }, + }, + }, + }, + `attr = { foo = keyword } +`, + hcl.Pos{Line: 1, Column: 11, Byte: 10}, + lang.CompleteCandidates([]lang.Candidate{ + { + Label: `foo`, + Detail: "optional, keyword", + TextEdit: lang.TextEdit{ + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 10, Byte: 9}, + End: hcl.Pos{Line: 1, Column: 23, Byte: 22}, + }, + NewText: `foo`, + Snippet: `foo = `, + }, + Kind: lang.AttributeCandidateKind, + TriggerSuggest: true, + }, + }), + }, + { + "single-line quoted element partial value in the middle of attribute name", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.Object{ + Attributes: schema.ObjectAttributes{ + "foo": { + IsOptional: true, + Constraint: schema.Keyword{ + Keyword: "keyword", + }, + }, + }, + }, + }, + }, + `attr = { "foo" = keyword } +`, + hcl.Pos{Line: 1, Column: 12, Byte: 11}, + lang.CompleteCandidates([]lang.Candidate{ + { + Label: `foo`, + Detail: "optional, keyword", + TextEdit: lang.TextEdit{ + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 10, Byte: 9}, + End: hcl.Pos{Line: 1, Column: 25, Byte: 24}, + }, + NewText: `foo`, + Snippet: `foo = `, + }, + Kind: lang.AttributeCandidateKind, + TriggerSuggest: true, + }, + }), + }, + { + "single-line quoted element partial value near the beginning of quote", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.Object{ + Attributes: schema.ObjectAttributes{ + "foo": { + IsOptional: true, + Constraint: schema.Keyword{ + Keyword: "keyword", + }, + }, + }, + }, + }, + }, + `attr = { "foo" = keyword } +`, + hcl.Pos{Line: 1, Column: 10, Byte: 9}, + lang.CompleteCandidates([]lang.Candidate{ + { + Label: `foo`, + Detail: "optional, keyword", + TextEdit: lang.TextEdit{ + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 10, Byte: 9}, + End: hcl.Pos{Line: 1, Column: 25, Byte: 24}, + }, + NewText: `foo`, + Snippet: `foo = `, + }, + Kind: lang.AttributeCandidateKind, + TriggerSuggest: true, + }, + }), + }, + { + "single-line before existing item", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.Object{ + Attributes: schema.ObjectAttributes{ + "foo": { + IsOptional: true, + Constraint: schema.Keyword{ + Keyword: "kw", + }, + }, + }, + }, + }, + }, + `attr = { foo = kw } +`, + hcl.Pos{Line: 1, Column: 10, Byte: 9}, + lang.CompleteCandidates([]lang.Candidate{}), + }, + { + "single-line after previous item with comma", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.Object{ + Attributes: schema.ObjectAttributes{ + "foo": { + IsOptional: true, + Constraint: schema.Keyword{ + Keyword: "kw", + }, + }, + "bar": { + IsOptional: true, + Constraint: schema.Keyword{ + Keyword: "kw2", + }, + }, + }, + }, + }, + }, + `attr = { foo = kw, } +`, + hcl.Pos{Line: 1, Column: 19, Byte: 18}, + lang.CompleteCandidates([]lang.Candidate{ + { + Label: `bar`, + Detail: "optional, keyword", + TextEdit: lang.TextEdit{ + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 19, Byte: 18}, + End: hcl.Pos{Line: 1, Column: 19, Byte: 18}, + }, + NewText: `bar`, + Snippet: `bar = `, + }, + Kind: lang.AttributeCandidateKind, + TriggerSuggest: true, + }, + }), + }, + { + "single-line after previous item without comma", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.Object{ + Attributes: schema.ObjectAttributes{ + "foo": { + IsOptional: true, + Constraint: schema.Keyword{ + Keyword: "kw", + }, + }, + "bar": { + IsOptional: true, + Constraint: schema.Keyword{ + Keyword: "kw2", + }, + }, + }, + }, + }, + }, + `attr = { foo = kw } +`, + hcl.Pos{Line: 1, Column: 19, Byte: 18}, + lang.CompleteCandidates([]lang.Candidate{}), + }, + { + "single-line between items with commas", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.Object{ + Attributes: schema.ObjectAttributes{ + "foo": { + IsOptional: true, + Constraint: schema.Keyword{ + Keyword: "kw", + }, + }, + "bar": { + IsOptional: true, + Constraint: schema.Keyword{ + Keyword: "kw", + }, + }, + "baz": { + IsOptional: true, + Constraint: schema.Keyword{ + Keyword: "kw2", + }, + }, + }, + }, + }, + }, + `attr = { foo = kw, , bar = kw } +`, + hcl.Pos{Line: 1, Column: 20, Byte: 19}, + lang.CompleteCandidates([]lang.Candidate{ + // Ideally bar attribute should be ignored here + // but because of the double comma the HCL parser ignores it + // TODO: Try to recover trailing configuration from remaining bytes? + { + Label: `bar`, + Detail: "optional, keyword", + TextEdit: lang.TextEdit{ + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 20, Byte: 19}, + End: hcl.Pos{Line: 1, Column: 20, Byte: 19}, + }, + NewText: `bar`, + Snippet: `bar = `, + }, + Kind: lang.AttributeCandidateKind, + TriggerSuggest: true, + }, + { + Label: `baz`, + Detail: "optional, keyword", + TextEdit: lang.TextEdit{ + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 20, Byte: 19}, + End: hcl.Pos{Line: 1, Column: 20, Byte: 19}, + }, + NewText: `baz`, + Snippet: `baz = `, + }, + Kind: lang.AttributeCandidateKind, + TriggerSuggest: true, + }, + }), + }, + { + "single-line between items without commas", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.Object{ + Attributes: schema.ObjectAttributes{ + "foo": { + IsOptional: true, + Constraint: schema.Keyword{ + Keyword: "kw", + }, + }, + "bar": { + IsOptional: true, + Constraint: schema.Keyword{ + Keyword: "kw", + }, + }, + "baz": { + IsOptional: true, + Constraint: schema.Keyword{ + Keyword: "kw2", + }, + }, + }, + }, + }, + }, + `attr = { foo = kw, bar = kw } +`, + hcl.Pos{Line: 1, Column: 20, Byte: 19}, + lang.CompleteCandidates([]lang.Candidate{}), + }, + + // multi line tests + { + "inside braces multi-line", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.Object{ + Attributes: schema.ObjectAttributes{ + "foo": { + IsOptional: true, + Constraint: schema.Keyword{ + Keyword: "kw", + }, + }, + "bar": { + IsOptional: true, + Constraint: schema.Keyword{ + Keyword: "kw", + }, + }, + "baz": { + IsOptional: true, + Constraint: schema.Keyword{ + Keyword: "kw2", + }, + }, + }, + }, + }, + }, + `attr = { + +} +`, + hcl.Pos{Line: 2, Column: 3, Byte: 11}, + lang.CompleteCandidates([]lang.Candidate{ + { + Label: `bar`, + Detail: "optional, keyword", + TextEdit: lang.TextEdit{ + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 2, Column: 3, Byte: 11}, + End: hcl.Pos{Line: 2, Column: 3, Byte: 11}, + }, + NewText: `bar`, + Snippet: `bar = `, + }, + Kind: lang.AttributeCandidateKind, + TriggerSuggest: true, + }, + { + Label: `baz`, + Detail: "optional, keyword", + TextEdit: lang.TextEdit{ + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 2, Column: 3, Byte: 11}, + End: hcl.Pos{Line: 2, Column: 3, Byte: 11}, + }, + NewText: `baz`, + Snippet: `baz = `, + }, + Kind: lang.AttributeCandidateKind, + TriggerSuggest: true, + }, + { + Label: `foo`, + Detail: "optional, keyword", + TextEdit: lang.TextEdit{ + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 2, Column: 3, Byte: 11}, + End: hcl.Pos{Line: 2, Column: 3, Byte: 11}, + }, + NewText: `foo`, + Snippet: `foo = `, + }, + Kind: lang.AttributeCandidateKind, + TriggerSuggest: true, + }, + }), + }, + { + "multi-line new element value", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.Object{ + Attributes: schema.ObjectAttributes{ + "foo": { + IsOptional: true, + Constraint: schema.Keyword{ + Keyword: "kw", + }, + }, + "bar": { + IsOptional: true, + Constraint: schema.Keyword{ + Keyword: "kw", + }, + }, + "baz": { + IsOptional: true, + Constraint: schema.Keyword{ + Keyword: "kw2", + }, + }, + }, + }, + }, + }, + `attr = { + foo = +} +`, + hcl.Pos{Line: 2, Column: 9, Byte: 17}, + lang.CompleteCandidates([]lang.Candidate{ + { + Label: `kw`, + Detail: "keyword", + TextEdit: lang.TextEdit{ + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 2, Column: 9, Byte: 17}, + End: hcl.Pos{Line: 2, Column: 9, Byte: 17}, + }, + NewText: `kw`, + Snippet: `kw`, + }, + Kind: lang.KeywordCandidateKind, + }, + }), + }, + { + "inside multi-line object partial attribute", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.Object{ + Attributes: schema.ObjectAttributes{ + "foo": { + Constraint: schema.Keyword{Keyword: "kw1"}, + IsOptional: true, + }, + "bar": { + Constraint: schema.Keyword{Keyword: "kw2"}, + IsOptional: true, + }, + }, + }, + }, + }, + `attr = { + b +}`, + hcl.Pos{Line: 2, Column: 4, Byte: 12}, + lang.CompleteCandidates([]lang.Candidate{ + { + Label: "bar", + Detail: "optional, keyword", + Kind: lang.AttributeCandidateKind, + TextEdit: lang.TextEdit{ + NewText: "bar", + Snippet: "bar = ", + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 2, Column: 3, Byte: 11}, + End: hcl.Pos{Line: 2, Column: 4, Byte: 12}, + }, + }, + TriggerSuggest: true, + }, + }), + }, + { + "inside multi-line partial new element value near end", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.Object{ + Attributes: schema.ObjectAttributes{ + "foo": { + IsOptional: true, + Constraint: schema.Keyword{ + Keyword: "kw", + }, + }, + "bar": { + IsOptional: true, + Constraint: schema.Keyword{ + Keyword: "kw1", + }, + }, + "baz": { + IsOptional: true, + Constraint: schema.Keyword{ + Keyword: "kw2", + }, + }, + }, + }, + }, + }, + `attr = { + foo = k +} +`, + hcl.Pos{Line: 2, Column: 10, Byte: 18}, + lang.CompleteCandidates([]lang.Candidate{ + { + Label: `kw`, + Detail: "keyword", + TextEdit: lang.TextEdit{ + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 2, Column: 9, Byte: 17}, + End: hcl.Pos{Line: 2, Column: 10, Byte: 18}, + }, + NewText: `kw`, + Snippet: `kw`, + }, + Kind: lang.KeywordCandidateKind, + }, + }), + }, + { + "inside multi-line partial new element value in the middle", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.Object{ + Attributes: schema.ObjectAttributes{ + "foo": { + IsOptional: true, + Constraint: schema.Keyword{ + Keyword: "keyword", + }, + }, + "bar": { + IsOptional: true, + Constraint: schema.Keyword{ + Keyword: "kw", + }, + }, + "baz": { + IsOptional: true, + Constraint: schema.Keyword{ + Keyword: "kw2", + }, + }, + }, + }, + }, + }, + `attr = { + foo = keyword +} +`, + hcl.Pos{Line: 2, Column: 12, Byte: 20}, + lang.CompleteCandidates([]lang.Candidate{ + { + Label: `keyword`, + Detail: "keyword", + TextEdit: lang.TextEdit{ + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 2, Column: 9, Byte: 17}, + End: hcl.Pos{Line: 2, Column: 16, Byte: 24}, + }, + NewText: `keyword`, + Snippet: `keyword`, + }, + Kind: lang.KeywordCandidateKind, + }, + }), + }, + { + "multi-line value after existing attribute", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.Object{ + Attributes: schema.ObjectAttributes{ + "foo": { + IsOptional: true, + Constraint: schema.Keyword{ + Keyword: "keyword", + }, + }, + "bar": { + IsOptional: true, + Constraint: schema.Keyword{ + Keyword: "kw", + }, + }, + "baz": { + IsOptional: true, + Constraint: schema.Keyword{ + Keyword: "kw2", + }, + }, + }, + }, + }, + }, + `attr = { + foo = keyword + bar = +} +`, + hcl.Pos{Line: 3, Column: 9, Byte: 33}, + lang.CompleteCandidates([]lang.Candidate{ + { + Label: `kw`, + Detail: "keyword", + TextEdit: lang.TextEdit{ + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 3, Column: 9, Byte: 33}, + End: hcl.Pos{Line: 3, Column: 9, Byte: 33}, + }, + NewText: `kw`, + Snippet: `kw`, + }, + Kind: lang.KeywordCandidateKind, + }, + }), + }, + { + "multi-line value before existing attribute", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.Object{ + Attributes: schema.ObjectAttributes{ + "foo": { + IsOptional: true, + Constraint: schema.Keyword{ + Keyword: "keyword", + }, + }, + "bar": { + IsOptional: true, + Constraint: schema.Keyword{ + Keyword: "kw", + }, + }, + "baz": { + IsOptional: true, + Constraint: schema.Keyword{ + Keyword: "kw2", + }, + }, + }, + }, + }, + }, + `attr = { + bar = + foo = keyword +} +`, + hcl.Pos{Line: 2, Column: 9, Byte: 17}, + lang.CompleteCandidates([]lang.Candidate{ + { + Label: `kw`, + Detail: "keyword", + TextEdit: lang.TextEdit{ + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 2, Column: 9, Byte: 17}, + End: hcl.Pos{Line: 2, Column: 9, Byte: 17}, + }, + NewText: `kw`, + Snippet: `kw`, + }, + Kind: lang.KeywordCandidateKind, + }, + }), + }, + { + "multi-line value before between attributes", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.Object{ + Attributes: schema.ObjectAttributes{ + "foo": { + IsOptional: true, + Constraint: schema.Keyword{ + Keyword: "keyword", + }, + }, + "bar": { + IsOptional: true, + Constraint: schema.Keyword{ + Keyword: "keyword", + }, + }, + "baz": { + IsOptional: true, + Constraint: schema.Keyword{ + Keyword: "keyword2", + }, + }, + }, + }, + }, + }, + `attr = { + bar = keyword + baz = + foo = keyword +} +`, + hcl.Pos{Line: 3, Column: 9, Byte: 33}, + lang.CompleteCandidates([]lang.Candidate{ + { + Label: `keyword2`, + Detail: "keyword", + TextEdit: lang.TextEdit{ + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 3, Column: 9, Byte: 33}, + End: hcl.Pos{Line: 3, Column: 9, Byte: 33}, + }, + NewText: `keyword2`, + Snippet: `keyword2`, + }, + Kind: lang.KeywordCandidateKind, + }, + }), + }, + { + "multi-line key between attributes", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.Object{ + Attributes: schema.ObjectAttributes{ + "foo": { + IsOptional: true, + Constraint: schema.Keyword{ + Keyword: "keyword", + }, + }, + "bar": { + IsOptional: true, + Constraint: schema.Keyword{ + Keyword: "keyword", + }, + }, + "baz": { + IsOptional: true, + Constraint: schema.Keyword{ + Keyword: "keyword2", + }, + }, + }, + }, + }, + }, + `attr = { + bar = keyword + baz + foo = keyword +} +`, + hcl.Pos{Line: 3, Column: 5, Byte: 29}, + lang.CompleteCandidates([]lang.Candidate{ + { + Label: `baz`, + Detail: "optional, keyword", + TextEdit: lang.TextEdit{ + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 3, Column: 3, Byte: 27}, + End: hcl.Pos{Line: 3, Column: 6, Byte: 30}, + }, + NewText: `baz`, + Snippet: `baz = `, + }, + Kind: lang.AttributeCandidateKind, + TriggerSuggest: true, + }, + }), + }, + { + "multi-line before attribute same line", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.Object{ + Attributes: schema.ObjectAttributes{ + "foo": { + IsOptional: true, + Constraint: schema.Keyword{ + Keyword: "keyword", + }, + }, + "bar": { + IsOptional: true, + Constraint: schema.Keyword{ + Keyword: "keyword", + }, + }, + "baz": { + IsOptional: true, + Constraint: schema.Keyword{ + Keyword: "keyword", + }, + }, + }, + }, + }, + }, + `attr = { + foo = keyword +} +`, + hcl.Pos{Line: 2, Column: 2, Byte: 10}, + lang.CompleteCandidates([]lang.Candidate{}), + }, + { + "multi-line before attribute separate line", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.Object{ + Attributes: schema.ObjectAttributes{ + "foo": { + IsOptional: true, + Constraint: schema.Keyword{ + Keyword: "keyword", + }, + }, + "bar": { + IsOptional: true, + Constraint: schema.Keyword{ + Keyword: "keyword", + }, + }, + "baz": { + IsOptional: true, + Constraint: schema.Keyword{ + Keyword: "keyword", + }, + }, + }, + }, + }, + }, + `attr = { + + foo = keyword +} +`, + hcl.Pos{Line: 2, Column: 3, Byte: 11}, + lang.CompleteCandidates([]lang.Candidate{ + { + Label: `bar`, + Detail: "optional, keyword", + TextEdit: lang.TextEdit{ + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 2, Column: 3, Byte: 11}, + End: hcl.Pos{Line: 2, Column: 3, Byte: 11}, + }, + NewText: `bar`, + Snippet: `bar = `, + }, + Kind: lang.AttributeCandidateKind, + TriggerSuggest: true, + }, + { + Label: `baz`, + Detail: "optional, keyword", + TextEdit: lang.TextEdit{ + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 2, Column: 3, Byte: 11}, + End: hcl.Pos{Line: 2, Column: 3, Byte: 11}, + }, + NewText: `baz`, + Snippet: `baz = `, + }, + Kind: lang.AttributeCandidateKind, + TriggerSuggest: true, + }, + }), + }, + { + "multi-line after attribute", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.Object{ + Attributes: schema.ObjectAttributes{ + "foo": { + IsOptional: true, + Constraint: schema.Keyword{ + Keyword: "keyword", + }, + }, + "bar": { + IsOptional: true, + Constraint: schema.Keyword{ + Keyword: "keyword", + }, + }, + "baz": { + IsOptional: true, + Constraint: schema.Keyword{ + Keyword: "keyword", + }, + }, + }, + }, + }, + }, + `attr = { + foo = keyword + +} +`, + hcl.Pos{Line: 3, Column: 3, Byte: 27}, + lang.CompleteCandidates([]lang.Candidate{ + { + Label: `bar`, + Detail: "optional, keyword", + TextEdit: lang.TextEdit{ + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 3, Column: 3, Byte: 27}, + End: hcl.Pos{Line: 3, Column: 3, Byte: 27}, + }, + NewText: `bar`, + Snippet: `bar = `, + }, + Kind: lang.AttributeCandidateKind, + TriggerSuggest: true, + }, + { + Label: `baz`, + Detail: "optional, keyword", + TextEdit: lang.TextEdit{ + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 3, Column: 3, Byte: 27}, + End: hcl.Pos{Line: 3, Column: 3, Byte: 27}, + }, + NewText: `baz`, + Snippet: `baz = `, + }, + Kind: lang.AttributeCandidateKind, + TriggerSuggest: true, + }, + }), + }, + { + "multi-line after attribute with comma newline", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.Object{ + Attributes: schema.ObjectAttributes{ + "foo": { + IsOptional: true, + Constraint: schema.Keyword{ + Keyword: "keyword", + }, + }, + "bar": { + IsOptional: true, + Constraint: schema.Keyword{ + Keyword: "keyword", + }, + }, + "baz": { + IsOptional: true, + Constraint: schema.Keyword{ + Keyword: "keyword", + }, + }, + }, + }, + }, + }, + `attr = { + foo = keyword, + +} +`, + hcl.Pos{Line: 3, Column: 3, Byte: 28}, + lang.CompleteCandidates([]lang.Candidate{ + { + Label: `bar`, + Detail: "optional, keyword", + TextEdit: lang.TextEdit{ + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 3, Column: 3, Byte: 28}, + End: hcl.Pos{Line: 3, Column: 3, Byte: 28}, + }, + NewText: `bar`, + Snippet: `bar = `, + }, + Kind: lang.AttributeCandidateKind, + TriggerSuggest: true, + }, + { + Label: `baz`, + Detail: "optional, keyword", + TextEdit: lang.TextEdit{ + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 3, Column: 3, Byte: 28}, + End: hcl.Pos{Line: 3, Column: 3, Byte: 28}, + }, + NewText: `baz`, + Snippet: `baz = `, + }, + Kind: lang.AttributeCandidateKind, + TriggerSuggest: true, + }, + }), + }, + { + "multi-line after attribute with comma same line", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.Object{ + Attributes: schema.ObjectAttributes{ + "foo": { + IsOptional: true, + Constraint: schema.Keyword{ + Keyword: "keyword", + }, + }, + "bar": { + IsOptional: true, + Constraint: schema.Keyword{ + Keyword: "keyword", + }, + }, + "baz": { + IsOptional: true, + Constraint: schema.Keyword{ + Keyword: "keyword", + }, + }, + }, + }, + }, + }, + `attr = { + foo = keyword, +} +`, + hcl.Pos{Line: 2, Column: 18, Byte: 26}, + lang.CompleteCandidates([]lang.Candidate{ + { + Label: `bar`, + Detail: "optional, keyword", + TextEdit: lang.TextEdit{ + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 2, Column: 18, Byte: 26}, + End: hcl.Pos{Line: 2, Column: 18, Byte: 26}, + }, + NewText: `bar`, + Snippet: `bar = `, + }, + Kind: lang.AttributeCandidateKind, + TriggerSuggest: true, + }, + { + Label: `baz`, + Detail: "optional, keyword", + TextEdit: lang.TextEdit{ + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 2, Column: 18, Byte: 26}, + End: hcl.Pos{Line: 2, Column: 18, Byte: 26}, + }, + NewText: `baz`, + Snippet: `baz = `, + }, + Kind: lang.AttributeCandidateKind, + TriggerSuggest: true, + }, + }), + }, + { + "multi-line after attribute without comma same line", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.Object{ + Attributes: schema.ObjectAttributes{ + "foo": { + IsOptional: true, + Constraint: schema.Keyword{ + Keyword: "keyword", + }, + }, + "bar": { + IsOptional: true, + Constraint: schema.Keyword{ + Keyword: "keyword", + }, + }, + "baz": { + IsOptional: true, + Constraint: schema.Keyword{ + Keyword: "keyword", + }, + }, + }, + }, + }, + }, + `attr = { + foo = keyword +} +`, + hcl.Pos{Line: 2, Column: 17, Byte: 25}, + lang.CompleteCandidates([]lang.Candidate{}), + }, + { + "multi-line between attributes without commas", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.Object{ + Attributes: schema.ObjectAttributes{ + "foo": { + IsOptional: true, + Constraint: schema.Keyword{ + Keyword: "keyword", + }, + }, + "bar": { + IsOptional: true, + Constraint: schema.Keyword{ + Keyword: "keyword", + }, + }, + "baz": { + IsOptional: true, + Constraint: schema.Keyword{ + Keyword: "keyword2", + }, + }, + }, + }, + }, + }, + `attr = { + foo = keyword + + bar = keyword +} +`, + hcl.Pos{Line: 3, Column: 3, Byte: 27}, + lang.CompleteCandidates([]lang.Candidate{ + { + Label: `baz`, + Detail: "optional, keyword", + TextEdit: lang.TextEdit{ + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 3, Column: 3, Byte: 27}, + End: hcl.Pos{Line: 3, Column: 3, Byte: 27}, + }, + NewText: `baz`, + Snippet: `baz = `, + }, + Kind: lang.AttributeCandidateKind, + TriggerSuggest: true, + }, + }), + }, + { + "multi-line between attributes with comma", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.Object{ + Attributes: schema.ObjectAttributes{ + "foo": { + IsOptional: true, + Constraint: schema.Keyword{ + Keyword: "keyword", + }, + }, + "bar": { + IsOptional: true, + Constraint: schema.Keyword{ + Keyword: "keyword", + }, + }, + "baz": { + IsOptional: true, + Constraint: schema.Keyword{ + Keyword: "keyword", + }, + }, + }, + }, + }, + }, + `attr = { + foo = keyword, + bar = keyword +} +`, + hcl.Pos{Line: 2, Column: 18, Byte: 26}, + lang.CompleteCandidates([]lang.Candidate{ + { + Label: `baz`, + Detail: "optional, keyword", + TextEdit: lang.TextEdit{ + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 2, Column: 18, Byte: 26}, + End: hcl.Pos{Line: 2, Column: 18, Byte: 26}, + }, + NewText: `baz`, + Snippet: `baz = `, + }, + Kind: lang.AttributeCandidateKind, + TriggerSuggest: true, + }, + }), + }, + } + 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.Logf("position: %#v in config: %s", tc.pos, tc.cfg) + t.Fatalf("unexpected candidates: %s", diff) + } + }) + } +} diff --git a/decoder/expression_test.go b/decoder/expression_test.go index 475f180c..0f9f3f60 100644 --- a/decoder/expression_test.go +++ b/decoder/expression_test.go @@ -7,7 +7,6 @@ var ( _ Expression = LiteralType{} _ Expression = LiteralValue{} _ Expression = Map{} - _ Expression = ObjectAttributes{} _ Expression = Object{} _ Expression = Set{} _ Expression = Reference{} @@ -18,7 +17,6 @@ var ( _ ReferenceOriginsExpression = List{} _ ReferenceOriginsExpression = LiteralType{} _ ReferenceOriginsExpression = Map{} - _ ReferenceOriginsExpression = ObjectAttributes{} _ ReferenceOriginsExpression = Object{} _ ReferenceOriginsExpression = Set{} _ ReferenceOriginsExpression = Reference{} @@ -28,7 +26,6 @@ var ( _ ReferenceTargetsExpression = List{} _ ReferenceTargetsExpression = LiteralType{} _ ReferenceTargetsExpression = Map{} - _ ReferenceTargetsExpression = ObjectAttributes{} _ ReferenceTargetsExpression = Object{} _ ReferenceTargetsExpression = Reference{} _ ReferenceTargetsExpression = Tuple{}