From 3ffca3530d824ba9b10c287552c44c7881343e0d Mon Sep 17 00:00:00 2001 From: Radek Simko Date: Wed, 4 Jan 2023 15:04:44 +0000 Subject: [PATCH 1/4] schema: Implement EmptyCompletionData for Keyword --- schema/constraint_keyword.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/schema/constraint_keyword.go b/schema/constraint_keyword.go index 17d766fd..079abecf 100644 --- a/schema/constraint_keyword.go +++ b/schema/constraint_keyword.go @@ -37,6 +37,8 @@ func (k Keyword) Copy() Constraint { } func (k Keyword) EmptyCompletionData(nextPlaceholder int) CompletionData { - // TODO - return CompletionData{} + return CompletionData{ + TriggerSuggest: true, + LastPlaceholder: nextPlaceholder, + } } From e583af59d77df2ce747bfa0d96381a850faf73ed Mon Sep 17 00:00:00 2001 From: Radek Simko Date: Tue, 24 Jan 2023 13:48:55 +0200 Subject: [PATCH 2/4] decoder: Implement completion for Keyword --- decoder/expr_keyword.go | 5 - decoder/expr_keyword_completion.go | 62 +++++++++++ decoder/expr_keyword_completion_test.go | 138 ++++++++++++++++++++++++ 3 files changed, 200 insertions(+), 5 deletions(-) create mode 100644 decoder/expr_keyword_completion.go create mode 100644 decoder/expr_keyword_completion_test.go diff --git a/decoder/expr_keyword.go b/decoder/expr_keyword.go index 01563618..1c49fa8b 100644 --- a/decoder/expr_keyword.go +++ b/decoder/expr_keyword.go @@ -13,11 +13,6 @@ type Keyword struct { cons schema.Keyword } -func (kw Keyword) CompletionAtPos(ctx context.Context, pos hcl.Pos) []lang.Candidate { - // TODO - return nil -} - func (kw Keyword) HoverAtPos(ctx context.Context, pos hcl.Pos) *lang.HoverData { // TODO return nil diff --git a/decoder/expr_keyword_completion.go b/decoder/expr_keyword_completion.go new file mode 100644 index 00000000..710c1459 --- /dev/null +++ b/decoder/expr_keyword_completion.go @@ -0,0 +1,62 @@ +package decoder + +import ( + "context" + "strings" + + "github.com/hashicorp/hcl-lang/lang" + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hclsyntax" +) + +func (kw Keyword) CompletionAtPos(ctx context.Context, pos hcl.Pos) []lang.Candidate { + if isEmptyExpression(kw.expr) { + return []lang.Candidate{ + { + Label: kw.cons.Keyword, + Detail: kw.cons.FriendlyName(), + Description: kw.cons.Description, + Kind: lang.KeywordCandidateKind, + TextEdit: lang.TextEdit{ + NewText: kw.cons.Keyword, + Snippet: kw.cons.Keyword, + Range: hcl.Range{ + Filename: kw.expr.Range().Filename, + Start: pos, + End: pos, + }, + }, + }, + } + } + + eType, ok := kw.expr.(*hclsyntax.ScopeTraversalExpr) + if !ok { + return []lang.Candidate{} + } + + if len(eType.Traversal) != 1 { + return []lang.Candidate{} + } + + prefixLen := pos.Byte - eType.Traversal.SourceRange().Start.Byte + prefix := eType.Traversal.RootName()[0:prefixLen] + + if strings.HasPrefix(kw.cons.Keyword, prefix) { + return []lang.Candidate{ + { + Label: kw.cons.Keyword, + Detail: kw.cons.FriendlyName(), + Description: kw.cons.Description, + Kind: lang.KeywordCandidateKind, + TextEdit: lang.TextEdit{ + NewText: kw.cons.Keyword, + Snippet: kw.cons.Keyword, + Range: eType.Range(), + }, + }, + } + } + + return []lang.Candidate{} +} diff --git a/decoder/expr_keyword_completion_test.go b/decoder/expr_keyword_completion_test.go new file mode 100644 index 00000000..2b2db6dd --- /dev/null +++ b/decoder/expr_keyword_completion_test.go @@ -0,0 +1,138 @@ +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_exprKeyword(t *testing.T) { + testCases := []struct { + testName string + attrSchema map[string]*schema.AttributeSchema + cfg string + pos hcl.Pos + expectedCandidates lang.Candidates + }{ + { + "no expression", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.Keyword{Keyword: "foobar"}, + }, + }, + `attr = `, + hcl.Pos{Line: 1, Column: 8, Byte: 7}, + lang.CompleteCandidates([]lang.Candidate{ + { + Label: "foobar", + Detail: "keyword", + Kind: lang.KeywordCandidateKind, + TextEdit: lang.TextEdit{ + NewText: "foobar", + Snippet: "foobar", + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + End: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + }, + }, + }, + }), + }, + { + "matching prefix", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.Keyword{Keyword: "foobar"}, + }, + }, + `attr = f`, + hcl.Pos{Line: 1, Column: 9, Byte: 8}, + lang.CompleteCandidates([]lang.Candidate{ + { + Label: "foobar", + Detail: "keyword", + Kind: lang.KeywordCandidateKind, + TextEdit: lang.TextEdit{ + NewText: "foobar", + Snippet: "foobar", + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + End: hcl.Pos{Line: 1, Column: 9, Byte: 8}, + }, + }, + }, + }), + }, + { + "matching prefix in the middle", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.Keyword{Keyword: "foobar"}, + }, + }, + `attr = foo`, + hcl.Pos{Line: 1, Column: 9, Byte: 8}, + lang.CompleteCandidates([]lang.Candidate{ + { + Label: "foobar", + Detail: "keyword", + Kind: lang.KeywordCandidateKind, + TextEdit: lang.TextEdit{ + NewText: "foobar", + Snippet: "foobar", + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + End: hcl.Pos{Line: 1, Column: 11, Byte: 10}, + }, + }, + }, + }), + }, + { + "mismatching prefix", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.Keyword{Keyword: "foobar"}, + }, + }, + `attr = x`, + hcl.Pos{Line: 1, Column: 9, Byte: 8}, + lang.CompleteCandidates([]lang.Candidate{}), + }, + } + 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) + } + }) + } +} From e9c161437e78bae52a0a09737c962ef3d8fc8586 Mon Sep 17 00:00:00 2001 From: Radek Simko Date: Tue, 24 Jan 2023 13:49:06 +0200 Subject: [PATCH 3/4] decoder: Implement hover for Keyword --- decoder/expr_keyword.go | 5 -- decoder/expr_keyword_hover.go | 35 +++++++++ decoder/expr_keyword_hover_test.go | 117 +++++++++++++++++++++++++++++ 3 files changed, 152 insertions(+), 5 deletions(-) create mode 100644 decoder/expr_keyword_hover.go create mode 100644 decoder/expr_keyword_hover_test.go diff --git a/decoder/expr_keyword.go b/decoder/expr_keyword.go index 1c49fa8b..8c66655d 100644 --- a/decoder/expr_keyword.go +++ b/decoder/expr_keyword.go @@ -13,11 +13,6 @@ type Keyword struct { cons schema.Keyword } -func (kw Keyword) HoverAtPos(ctx context.Context, pos hcl.Pos) *lang.HoverData { - // TODO - return nil -} - func (kw Keyword) SemanticTokens(ctx context.Context) []lang.SemanticToken { // TODO return nil diff --git a/decoder/expr_keyword_hover.go b/decoder/expr_keyword_hover.go new file mode 100644 index 00000000..5cb80dbd --- /dev/null +++ b/decoder/expr_keyword_hover.go @@ -0,0 +1,35 @@ +package decoder + +import ( + "context" + "fmt" + + "github.com/hashicorp/hcl-lang/lang" + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hclsyntax" +) + +func (kw Keyword) HoverAtPos(ctx context.Context, pos hcl.Pos) *lang.HoverData { + eType, ok := kw.expr.(*hclsyntax.ScopeTraversalExpr) + if !ok { + return nil + } + + if len(eType.Traversal) != 1 { + return nil + } + + if eType.Traversal.RootName() == kw.cons.Keyword { + content := fmt.Sprintf("`%s` _%s_", kw.cons.Keyword, kw.cons.FriendlyName()) + if kw.cons.Description.Value != "" { + content += "\n\n" + kw.cons.Description.Value + } + + return &lang.HoverData{ + Content: lang.Markdown(content), + Range: eType.SrcRange, + } + } + + return nil +} diff --git a/decoder/expr_keyword_hover_test.go b/decoder/expr_keyword_hover_test.go new file mode 100644 index 00000000..d900ed0e --- /dev/null +++ b/decoder/expr_keyword_hover_test.go @@ -0,0 +1,117 @@ +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 TestHoverAtPos_exprKeyword(t *testing.T) { + testCases := []struct { + testName string + attrSchema map[string]*schema.AttributeSchema + cfg string + pos hcl.Pos + expectedHoverData *lang.HoverData + }{ + { + "mismatching expression type", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.Keyword{ + Keyword: "foobar", + }, + }, + }, + `attr = "foobar"`, + hcl.Pos{Line: 1, Column: 12, Byte: 11}, + nil, + }, + { + "mismatching keyword", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.Keyword{ + Keyword: "foobar", + }, + }, + }, + `attr = barfoo`, + hcl.Pos{Line: 1, Column: 12, Byte: 11}, + nil, + }, + { + "matching keyword", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.Keyword{ + Keyword: "foobar", + }, + }, + }, + `attr = foobar`, + hcl.Pos{Line: 1, Column: 12, Byte: 11}, + &lang.HoverData{ + Content: lang.Markdown("`foobar` _keyword_"), + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + End: hcl.Pos{Line: 1, Column: 14, Byte: 13}, + }, + }, + }, + { + "matching keyword with all metadata", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.Keyword{ + Keyword: "foobar", + Name: "custom name", + Description: lang.Markdown("custom _description_"), + }, + }, + }, + `attr = foobar`, + hcl.Pos{Line: 1, Column: 12, Byte: 11}, + &lang.HoverData{ + Content: lang.Markdown("`foobar` _custom name_\n\ncustom _description_"), + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + End: hcl.Pos{Line: 1, Column: 14, Byte: 13}, + }, + }, + }, + } + 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() + hoverData, err := d.HoverAtPos(ctx, "test.tf", tc.pos) + if err != nil { + t.Fatal(err) + } + + if diff := cmp.Diff(tc.expectedHoverData, hoverData); diff != "" { + t.Fatalf("unexpected hover data: %s", diff) + } + }) + } +} From 1aaa58b151ff62c95a30a6bb871e17119f3bab1d Mon Sep 17 00:00:00 2001 From: Radek Simko Date: Tue, 24 Jan 2023 13:47:14 +0200 Subject: [PATCH 4/4] decoder: Implement semantic tokens for Keyword --- decoder/expr_keyword.go | 8 -- decoder/expr_keyword_semtok.go | 31 +++++++ decoder/expr_keyword_semtok_test.go | 123 ++++++++++++++++++++++++++++ 3 files changed, 154 insertions(+), 8 deletions(-) create mode 100644 decoder/expr_keyword_semtok.go create mode 100644 decoder/expr_keyword_semtok_test.go diff --git a/decoder/expr_keyword.go b/decoder/expr_keyword.go index 8c66655d..aebd3888 100644 --- a/decoder/expr_keyword.go +++ b/decoder/expr_keyword.go @@ -1,9 +1,6 @@ package decoder import ( - "context" - - "github.com/hashicorp/hcl-lang/lang" "github.com/hashicorp/hcl-lang/schema" "github.com/hashicorp/hcl/v2" ) @@ -12,8 +9,3 @@ type Keyword struct { expr hcl.Expression cons schema.Keyword } - -func (kw Keyword) SemanticTokens(ctx context.Context) []lang.SemanticToken { - // TODO - return nil -} diff --git a/decoder/expr_keyword_semtok.go b/decoder/expr_keyword_semtok.go new file mode 100644 index 00000000..cc065cb6 --- /dev/null +++ b/decoder/expr_keyword_semtok.go @@ -0,0 +1,31 @@ +package decoder + +import ( + "context" + + "github.com/hashicorp/hcl-lang/lang" + "github.com/hashicorp/hcl/v2/hclsyntax" +) + +func (kw Keyword) SemanticTokens(ctx context.Context) []lang.SemanticToken { + eType, ok := kw.expr.(*hclsyntax.ScopeTraversalExpr) + if !ok { + return []lang.SemanticToken{} + } + + if len(eType.Traversal) != 1 { + return []lang.SemanticToken{} + } + + if eType.Traversal.RootName() == kw.cons.Keyword { + return []lang.SemanticToken{ + { + Type: lang.TokenKeyword, + Modifiers: []lang.SemanticTokenModifier{}, + Range: eType.Range(), + }, + } + } + + return []lang.SemanticToken{} +} diff --git a/decoder/expr_keyword_semtok_test.go b/decoder/expr_keyword_semtok_test.go new file mode 100644 index 00000000..4911ee98 --- /dev/null +++ b/decoder/expr_keyword_semtok_test.go @@ -0,0 +1,123 @@ +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 TestSemanticTokens_exprKeyword(t *testing.T) { + testCases := []struct { + testName string + attrSchema map[string]*schema.AttributeSchema + cfg string + expectedSemanticTokens []lang.SemanticToken + }{ + { + "mismatching expression type", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.Keyword{ + Keyword: "foobar", + }, + }, + }, + `attr = "foobar"`, + []lang.SemanticToken{ + { + Type: "hcl-attrName", + Modifiers: lang.SemanticTokenModifiers{}, + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 1, Column: 5, Byte: 4}, + }, + }, + }, + }, + { + "mismatching keyword", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.Keyword{ + Keyword: "foobar", + }, + }, + }, + `attr = barfoo`, + []lang.SemanticToken{ + { + Type: "hcl-attrName", + Modifiers: lang.SemanticTokenModifiers{}, + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 1, Column: 5, Byte: 4}, + }, + }, + }, + }, + { + "matching keyword", + map[string]*schema.AttributeSchema{ + "attr": { + Constraint: schema.Keyword{ + Keyword: "foobar", + }, + }, + }, + `attr = foobar`, + []lang.SemanticToken{ + { + Type: "hcl-attrName", + Modifiers: lang.SemanticTokenModifiers{}, + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 1, Column: 5, Byte: 4}, + }, + }, + { + Type: "hcl-keyword", + Modifiers: lang.SemanticTokenModifiers{}, + Range: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + End: hcl.Pos{Line: 1, Column: 14, Byte: 13}, + }, + }, + }, + }, + } + 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() + tokens, err := d.SemanticTokensInFile(ctx, "test.tf") + if err != nil { + t.Fatal(err) + } + + if diff := cmp.Diff(tc.expectedSemanticTokens, tokens); diff != "" { + t.Fatalf("unexpected tokens: %s", diff) + } + }) + } +}