Skip to content

Commit

Permalink
implement generic code lens interface
Browse files Browse the repository at this point in the history
  • Loading branch information
radeksimko committed Oct 28, 2021
1 parent 6e8883e commit f66b386
Show file tree
Hide file tree
Showing 6 changed files with 207 additions and 0 deletions.
25 changes: 25 additions & 0 deletions decoder/code_lens.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package decoder

import (
"context"

"github.com/hashicorp/hcl-lang/lang"
)

func (d *PathDecoder) CodeLensesForFile(ctx context.Context, file string) ([]lang.CodeLens, error) {
lenses := make([]lang.CodeLens, 0)

// TODO: multierror

for _, clFunc := range d.decoderCtx.CodeLenses {
ctx = withPathContext(ctx, d.pathCtx)

cls, err := clFunc(ctx, d.path, file)
if err != nil {
return lenses, err
}
lenses = append(lenses, cls...)
}

return lenses, nil
}
4 changes: 4 additions & 0 deletions decoder/context.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package decoder

import "github.com/hashicorp/hcl-lang/lang"

type DecoderContext struct {
// UTM parameters for docs URLs
// utm_source parameter, typically language server identification
Expand All @@ -8,6 +10,8 @@ type DecoderContext struct {
UtmMedium string
// utm_content parameter, e.g. documentHover or documentLink
UseUtmContent bool

CodeLenses []lang.CodeLensFunc
}

func (d *Decoder) SetContext(ctx DecoderContext) {
Expand Down
17 changes: 17 additions & 0 deletions decoder/path_context.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package decoder

import (
"context"
"fmt"

"github.com/hashicorp/hcl-lang/reference"
"github.com/hashicorp/hcl-lang/schema"
"github.com/hashicorp/hcl/v2"
Expand All @@ -12,3 +15,17 @@ type PathContext struct {
ReferenceTargets reference.Targets
Files map[string]*hcl.File
}

type pathCtxKey struct{}

func withPathContext(ctx context.Context, pathCtx *PathContext) context.Context {
return context.WithValue(ctx, pathCtxKey{}, pathCtx)
}

func PathCtx(ctx context.Context) (*PathContext, error) {
pathCtx, ok := ctx.Value(pathCtxKey{}).(*PathContext)
if !ok {
return nil, fmt.Errorf("path context not found")
}
return pathCtx, nil
}
25 changes: 25 additions & 0 deletions lang/code_lens.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package lang

import (
"context"
"encoding/json"

"github.com/hashicorp/hcl/v2"
)

type CodeLensFunc func(ctx context.Context, path Path, file string) ([]CodeLens, error)

type CodeLens struct {
Range hcl.Range
Command Command
}

type Command struct {
Title string
ID string
Arguments []CommandArgument
}

type CommandArgument interface {
AsJSON() (json.RawMessage, error)
}
129 changes: 129 additions & 0 deletions stdlib/codelens/reference_count.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
package codelens

import (
"context"
"encoding/json"
"fmt"

"github.com/hashicorp/hcl-lang/decoder"
"github.com/hashicorp/hcl-lang/lang"
"github.com/hashicorp/hcl-lang/reference"
"github.com/hashicorp/hcl/v2"
)

func ReferenceCount(showReferencesCmdId string, refCtx ReferenceContext) lang.CodeLensFunc {
return func(ctx context.Context, path lang.Path, file string) ([]lang.CodeLens, error) {
lenses := make([]lang.CodeLens, 0)

pathCtx, err := decoder.PathCtx(ctx)
if err != nil {
return nil, err
}

refTargets := pathCtx.ReferenceTargets.OutermostInFile(file)
if err != nil {
return nil, err
}

// There can be two targets pointing to the same range
// e.g. when a block is targetable as type-less reference
// and as an object, which is important in most contexts
// but not here, where we present it to the user.
dedupedTargets := make(map[hcl.Range]reference.Targets, 0)
for _, refTarget := range refTargets {
rng := *refTarget.RangePtr
if _, ok := dedupedTargets[rng]; !ok {
dedupedTargets[rng] = make(reference.Targets, 0)
}
dedupedTargets[rng] = append(dedupedTargets[rng], refTarget)
}

for rng, refTargets := range dedupedTargets {
originCount := 0
var defRange *hcl.Range
for _, refTarget := range refTargets {
if refTarget.DefRangePtr != nil {
defRange = refTarget.DefRangePtr
}

originCount += len(pathCtx.ReferenceOrigins.Targeting(refTarget))
}

if originCount == 0 {
continue
}

var hclPos hcl.Pos
if defRange != nil {
hclPos = posMiddleOfRange(defRange)
} else {
hclPos = posMiddleOfRange(&rng)
}

lenses = append(lenses, lang.CodeLens{
Range: rng,
Command: lang.Command{
Title: getTitle("reference", "references", originCount),
ID: showReferencesCmdId,
Arguments: []lang.CommandArgument{
hclAsJsonPos(hclPos),
refCtx,
},
},
})
}
return lenses, nil
}
}

func hclAsJsonPos(pos hcl.Pos) jsonPos {
return jsonPos{
Line: pos.Line,
Column: pos.Column,
Byte: pos.Byte,
}
}

type jsonPos struct {
Line int `json:"line"`
Column int `json:"column"`
Byte int `json:"byte"`
}

func (p jsonPos) AsJSON() (json.RawMessage, error) {
b, err := json.Marshal(p)
return json.RawMessage(b), err
}

type ReferenceContext struct {
IncludeDeclaration bool `json:"includeDeclaration"`
}

func (refCtx ReferenceContext) AsJSON() (json.RawMessage, error) {
b, err := json.Marshal(refCtx)
return json.RawMessage(b), err
}

func posMiddleOfRange(rng *hcl.Range) hcl.Pos {
col := rng.Start.Column
byte := rng.Start.Byte

if rng.Start.Line == rng.End.Line && rng.End.Column > rng.Start.Column {
charsFromStart := (rng.End.Column - rng.Start.Column) / 2
col += charsFromStart
byte += charsFromStart
}

return hcl.Pos{
Line: rng.Start.Line,
Column: col,
Byte: byte,
}
}

func getTitle(singular, plural string, n int) string {
if n > 1 || n == 0 {
return fmt.Sprintf("%d %s", n, plural)
}
return fmt.Sprintf("%d %s", n, singular)
}
7 changes: 7 additions & 0 deletions stdlib/codelens/reference_count_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package codelens

import "testing"

func TestReferenceCount(t *testing.T) {
t.Fatal("TODO")
}

0 comments on commit f66b386

Please sign in to comment.