Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
ansgarm committed Feb 28, 2024
1 parent 9935743 commit ed22080
Show file tree
Hide file tree
Showing 3 changed files with 92 additions and 1 deletion.
52 changes: 51 additions & 1 deletion decoder/candidates.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,57 @@ func (d *PathDecoder) completionAtPos(ctx context.Context, body *hclsyntax.Body,
// TODO: handle nil Expr in all nested calls instead which allows us
// to recover incomplete calls to provider defined functions (which have no expression
// as they are deemed invalid by hcl while they are not completed yet)
if attr.Expr != nil && d.isPosInsideAttrExpr(attr, pos) {

// attempt to recover incomplete provider defined function calls
// TODO: figure out if there's a better place to put this (possibly nested)
// if attr.Expr == nil && attr.SrcRange.ContainsPos(pos) {
// // we are somewhere in the range for this attribute but we don't have an expression range to check
// // so we look back to check whether we are in a partially written provider defined function
// fileBytes := d.pathCtx.Files[attr.SrcRange.Filename].Bytes

// recoveredPrefixBytes := recoverLeftBytes(fileBytes, pos, func(offset int, r rune) bool {
// return !isNamespacedFunctionNameRune(r)
// })
// // recoveredPrefixBytes also contains the rune before the function name, so we need to trim it
// _, lengthFirstRune := utf8.DecodeRune(recoveredPrefixBytes)
// recoveredPrefixBytes = recoveredPrefixBytes[lengthFirstRune:]

// recoveredSuffixBytes := recoverRightBytes(fileBytes, pos, func(offset int, r rune) bool {
// return !isNamespacedFunctionNameRune(r) && r != '('
// })
// // recoveredSuffixBytes also contains the rune after the function name, so we need to trim it
// _, lengthLastRune := utf8.DecodeLastRune(recoveredSuffixBytes)
// recoveredSuffixBytes = recoveredSuffixBytes[:len(recoveredSuffixBytes)-lengthLastRune]

// // check if our recovered string starts with provider:
// // Why just one colon? For no colons the parser would return a traversal expression
// // and a single colon will be the first prefix of a future provider defined function
// // TODO: this fails for completions for provider<>:: with the cursor at <> (and previous cursor positions)
// // fixing this would also need a lookahead for a colon after the cursor position (and maybe more chars of "provider")
// if bytes.HasPrefix(recoveredPrefixBytes, []byte("provider:")) {
// print(recoveredPrefixBytes)
// print(recoveredSuffixBytes)
// // TODO: recover the full expression to get the entire name

// // TODO: is this smart? 🧐
// attr.Expr = &hclsyntax.FunctionCallExpr{
// Name: string(recoveredPrefixBytes),
// NameRange: hcl.Range{
// Filename: attr.SrcRange.Filename,
// Start: hcl.Pos{
// Byte: attr.SrcRange.Start.Byte - len(recoveredPrefixBytes),
// }, // TODO: wrong
// End: pos,
// },
// }

// // FIXME: put recovery into function expr CompletionAtPos -> handle nil expr everywhere else (all expressions)

// }

// }

if (attr.Expr != nil && d.isPosInsideAttrExpr(attr, pos)) || (attr.Expr == nil && attr.SrcRange.ContainsPos(pos)) {
if bodySchema.Extensions != nil && bodySchema.Extensions.SelfRefs {
ctx = schema.WithActiveSelfRefs(ctx)
}
Expand Down
4 changes: 4 additions & 0 deletions decoder/expr_any_completion.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ import (
func (a Any) CompletionAtPos(ctx context.Context, pos hcl.Pos) []lang.Candidate {
typ := a.cons.OfType

if a.expr == nil {
return []lang.Candidate{}
}

if !a.cons.SkipLiteralComplexTypes && typ.IsListType() {
expr, ok := a.expr.(*hclsyntax.TupleConsExpr)
if !ok {
Expand Down
37 changes: 37 additions & 0 deletions decoder/expression.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package decoder

import (
"context"
"unicode"
"unicode/utf8"

"github.com/hashicorp/hcl-lang/lang"
Expand Down Expand Up @@ -253,13 +254,49 @@ func recoverLeftBytes(b []byte, pos hcl.Pos, f func(byteOffset int, r rune) bool
return []byte{}
}

// recoverRightBytes seeks right from given pos in given slice of bytes
// and recovers all bytes up until f matches, including that match.
// This allows recovery of incomplete configuration which is not
// present in the parsed AST during completion.
//
// Zero bytes is returned if no match was found.
func recoverRightBytes(b []byte, pos hcl.Pos, f func(byteOffset int, r rune) bool) []byte {
nextRune, size := utf8.DecodeRune(b[pos.Byte:])
offset := pos.Byte + size

// check for early match
if f(pos.Byte, nextRune) {
return b[pos.Byte:offset]
}

for offset < len(b) {
nextRune, size := utf8.DecodeRune(b[offset:])
if f(offset, nextRune) {
// record the matched offset
// and include the matched last rune
endByte := offset + size
return b[pos.Byte:endByte]
}
offset += size
}

return []byte{}
}

// isObjectItemTerminatingRune returns true if the given rune
// is considered a left terminating character for an item
// in hclsyntax.ObjectConsExpr.
func isObjectItemTerminatingRune(r rune) bool {
return r == '\n' || r == ',' || r == '{'
}

// isNamespacedFunctionNameRune returns true if the given run
// is a valid character of a namespaced function name.
// This includes letters, digits, dashes, underscores, and colons.
func isNamespacedFunctionNameRune(r rune) bool {
return unicode.IsLetter(r) || unicode.IsDigit(r) || r == '-' || r == '_' || r == ':'
}

// rawObjectKey extracts raw key (as string) from KeyExpr of
// any hclsyntax.ObjectConsExpr along with the corresponding range
// and boolean indicating whether the extraction was successful.
Expand Down

0 comments on commit ed22080

Please sign in to comment.