diff --git a/decoder/code_lens.go b/decoder/code_lens.go new file mode 100644 index 00000000..35270c26 --- /dev/null +++ b/decoder/code_lens.go @@ -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 +} diff --git a/decoder/context.go b/decoder/context.go index 4fbf9934..c3aac3c2 100644 --- a/decoder/context.go +++ b/decoder/context.go @@ -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 @@ -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) { diff --git a/decoder/path_context.go b/decoder/path_context.go index b6545dd8..ebaec109 100644 --- a/decoder/path_context.go +++ b/decoder/path_context.go @@ -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" @@ -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 +} diff --git a/lang/code_lens.go b/lang/code_lens.go new file mode 100644 index 00000000..c4abb642 --- /dev/null +++ b/lang/code_lens.go @@ -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) +} diff --git a/stdlib/codelens/reference_count.go b/stdlib/codelens/reference_count.go new file mode 100644 index 00000000..bfef6799 --- /dev/null +++ b/stdlib/codelens/reference_count.go @@ -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) +} diff --git a/stdlib/codelens/reference_count_test.go b/stdlib/codelens/reference_count_test.go new file mode 100644 index 00000000..5f0bc08d --- /dev/null +++ b/stdlib/codelens/reference_count_test.go @@ -0,0 +1,7 @@ +package codelens + +import "testing" + +func TestReferenceCount(t *testing.T) { + t.Fatal("TODO") +}