diff --git a/decoder/expression.go b/decoder/expression.go index 4e3d8670..47fcbb93 100644 --- a/decoder/expression.go +++ b/decoder/expression.go @@ -2,6 +2,7 @@ package decoder import ( "context" + "unicode/utf8" "github.com/hashicorp/hcl-lang/lang" "github.com/hashicorp/hcl-lang/reference" @@ -167,3 +168,39 @@ func newEmptyExpressionAtPos(filename string, pos hcl.Pos) hcl.Expression { }, } } + +// recoverLeftBytes seeks left 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 recoverLeftBytes(b []byte, pos hcl.Pos, f func(byteOffset int, r rune) bool) []byte { + firstRune, size := utf8.DecodeLastRune(b[:pos.Byte]) + offset := pos.Byte - size + + // check for early match + if f(pos.Byte, firstRune) { + return b[offset:pos.Byte] + } + + for offset > 0 { + nextRune, size := utf8.DecodeLastRune(b[:offset]) + if f(offset, nextRune) { + // record the matched offset + // and include the matched last rune + startByte := offset - size + return b[startByte:pos.Byte] + } + 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 == '{' +} diff --git a/decoder/expression_test.go b/decoder/expression_test.go index 475f180c..d449643d 100644 --- a/decoder/expression_test.go +++ b/decoder/expression_test.go @@ -1,5 +1,14 @@ package decoder +import ( + "bytes" + "fmt" + "testing" + "unicode" + + "github.com/hashicorp/hcl/v2" +) + var ( _ Expression = Any{} _ Expression = Keyword{} @@ -33,3 +42,57 @@ var ( _ ReferenceTargetsExpression = Reference{} _ ReferenceTargetsExpression = Tuple{} ) + +func TestRecoverLeftBytes(t *testing.T) { + testCases := []struct { + b []byte + pos hcl.Pos + f func(int, rune) bool + expectedBytes []byte + }{ + { + []byte(`toot foobar`), + hcl.Pos{Line: 1, Column: 13, Byte: 12}, + func(i int, r rune) bool { + return unicode.IsSpace(r) + }, + []byte(` foobar`), + }, + { + []byte(`hello👋world and other planets`), + hcl.Pos{Line: 1, Column: 15, Byte: 14}, + func(i int, r rune) bool { + return r == '👋' + }, + []byte(`👋world`), + }, + { + []byte(`hello world👋and other planets`), + hcl.Pos{Line: 1, Column: 16, Byte: 15}, + func(i int, r rune) bool { + return r == '👋' + }, + []byte(`👋`), + }, + { + []byte(`attr = { + foo = "foo", + bar = +} +`), + hcl.Pos{Line: 3, Column: 9, Byte: 32}, + func(i int, r rune) bool { + return r == '\n' || r == ',' + }, + []byte("\n bar = "), + }, + } + for i, tc := range testCases { + t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { + recoveredBytes := recoverLeftBytes(tc.b, tc.pos, tc.f) + if !bytes.Equal(tc.expectedBytes, recoveredBytes) { + t.Fatalf("mismatch!\nexpected: %q\nrecovered: %q\n", string(tc.expectedBytes), string(recoveredBytes)) + } + }) + } +}