From 9af1309eaaca2a13bb4334156070f4872c80bb61 Mon Sep 17 00:00:00 2001 From: Daniel Banck Date: Wed, 8 Feb 2023 18:15:49 +0100 Subject: [PATCH] WIP: any expr --- decoder/expr_any.go | 193 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 189 insertions(+), 4 deletions(-) diff --git a/decoder/expr_any.go b/decoder/expr_any.go index 2024eda1..682c6471 100644 --- a/decoder/expr_any.go +++ b/decoder/expr_any.go @@ -2,11 +2,16 @@ package decoder import ( "context" + "fmt" + "log" + "sort" + "strings" "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" ) type Any struct { @@ -16,17 +21,157 @@ type Any struct { } func (a Any) CompletionAtPos(ctx context.Context, pos hcl.Pos) []lang.Candidate { - // TODO - return nil + log.Printf("expression: %#v", a.expr) + if a.expr == nil || isEmptyExpression(a.expr) { + editRange := hcl.Range{ + Filename: a.expr.Range().Filename, + Start: pos, + End: pos, + } + + candidates := make([]lang.Candidate, 0) + candidates = append(candidates, a.matchingFunctions("", editRange)...) // TODO? create a function call expr + candidates = append(candidates, newExpression(a.pathCtx, a.expr, schema.Reference{OfType: a.cons.OfType}).CompletionAtPos(ctx, pos)...) + + return candidates + } + + switch e := a.expr.(type) { + case *hclsyntax.FunctionCallExpr: + if e.NameRange.ContainsPos(pos) { + prefixLen := pos.Byte - e.NameRange.Start.Byte + prefix := e.Name[0:prefixLen] + editRange := e.Range() + return a.matchingFunctions(prefix, editRange) + } + + f, ok := a.pathCtx.Functions[e.Name] + if ok { + parensRange := hcl.RangeBetween(e.OpenParenRange, e.CloseParenRange) + if parensRange.ContainsPos(pos) { + fParamsLength := len(f.Params) + // Function accepts no parameters + if fParamsLength == 0 && f.VarParam == nil { + return []lang.Candidate{} + } + + var activeParameterIdx int + var found bool + activeParameterIdx, found = searchActiveParameterByArgs(e.Args, pos) + if !found { + activeParameterIdx, found = searchActiveParameterByBytes(a.pathCtx, e.OpenParenRange, pos) + } + if !found { + return []lang.Candidate{} + } + + // arg := e.Args[activeParameterIdx] + // check the length + + // In case of a variadic parameter there might be more + // arguments supplied to the function than parameters + // of the signature + if activeParameterIdx >= fParamsLength { + // If we're not dealing with a variadic parameter, we + // can't suggest anything and the user is supplying too + // many arguments to the function + if f.VarParam == nil { + return []lang.Candidate{} + } + + // TODO! decide when to use empty expr + return newExpression( + a.pathCtx, + newEmptyExpressionAtPos(a.expr.Range().Filename, pos), + schema.AnyExpression{OfType: f.VarParam.Type}, + ).CompletionAtPos(ctx, pos) + } + + if fParamsLength > 0 { + activeParameter := f.Params[activeParameterIdx] + + return newExpression( + a.pathCtx, + newEmptyExpressionAtPos(a.expr.Range().Filename, pos), + schema.AnyExpression{OfType: activeParameter.Type}, + ).CompletionAtPos(ctx, pos) + } + } + } + + // TODO? handle TypeDeclarationExpr -- probably not + + return []lang.Candidate{} + } + + return []lang.Candidate{} +} + +func searchActiveParameterByArgs(args []hclsyntax.Expression, pos hcl.Pos) (int, bool) { + for i, v := range args { + if v.Range().ContainsPos(pos) { + return i, true + } + } + + return 0, false +} + +func searchActiveParameterByBytes(pathCtx *PathContext, openParenRange hcl.Range, pos hcl.Pos) (int, bool) { + f, ok := pathCtx.Files[openParenRange.Filename] + if !ok { + return 0, false + } + + rangeToPos := hcl.Range{ + Filename: openParenRange.Filename, + Start: openParenRange.Start, + End: pos, + } + + if !rangeToPos.CanSliceBytes(f.Bytes) { + return 0, false + } + + b := rangeToPos.SliceBytes(f.Bytes) + commasUntilPos := strings.Count(string(b), ",") + + return commasUntilPos, true } func (a Any) HoverAtPos(ctx context.Context, pos hcl.Pos) *lang.HoverData { - // TODO + switch e := a.expr.(type) { + case *hclsyntax.FunctionCallExpr: + f, ok := a.pathCtx.Functions[e.Name] + if ok { + return &lang.HoverData{ + // We could use a code block here, but the HCL highlighting of + // type names conflicts with the naming of function parameters + // e.g. a parameter with the name `list`` + Content: lang.Markdown(fmt.Sprintf("`%s(%s) %s`\n\n%s", e.Name, f.ParameterSignature(), f.ReturnType.FriendlyName(), f.Description)), + Range: a.expr.Range(), + } + } + } + return nil } func (a Any) SemanticTokens(ctx context.Context) []lang.SemanticToken { - // TODO + switch e := a.expr.(type) { + case *hclsyntax.FunctionCallExpr: + _, ok := a.pathCtx.Functions[e.Name] + if ok { + return []lang.SemanticToken{ + { + Type: lang.TokenAttrName, + Modifiers: []lang.SemanticTokenModifier{}, + // Range: eType.Range(), + }, + } + } + } + return nil } @@ -39,3 +184,43 @@ func (a Any) ReferenceTargets(ctx context.Context, addr lang.Address, addrCtx Ad // TODO return nil } + +func (a Any) matchingFunctions(prefix string, editRange hcl.Range) []lang.Candidate { + candidates := make([]lang.Candidate, 0) + + for name, f := range a.pathCtx.Functions { + // Only suggest functions that have a matching return type + // TODO? what about dynamic + if a.cons.OfType != f.ReturnType { + continue + } + + candidates = append(candidates, lang.Candidate{ + Label: name, + Detail: fmt.Sprintf("%s(%s) %s", name, f.ParameterSignature(), f.ReturnType.FriendlyName()), + Kind: lang.FunctionCandidateKind, + Description: lang.Markdown(f.Description), + TextEdit: lang.TextEdit{ + NewText: fmt.Sprintf("%s()", name), + Snippet: fmt.Sprintf("%s(${0})", name), // TODO? improve with parameter information + Range: editRange, + }, + }) + } + + sort.SliceStable(candidates, func(i, j int) bool { + return candidates[i].Label < candidates[j].Label + }) + + return candidates +} + +func (a Any) matchingTraversal(editRange hcl.Range) []lang.Candidate { + candidates := make([]lang.Candidate, 0) + + if a.pathCtx.ReferenceTargets == nil { + return candidates + } + + return candidates +}