Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support values lookup for range on mapping #86

Merged
merged 5 commits into from
May 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions internal/charts/values_files.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package charts

import (
"fmt"
"path/filepath"

"github.com/mrjosh/helm-ls/internal/util"
Expand Down Expand Up @@ -69,11 +70,14 @@ func (v *ValuesFiles) AllValuesFiles() []*ValuesFile {
}

func (v *ValuesFiles) GetPositionsForValue(query []string) []lsp.Location {
logger.Debug(fmt.Sprintf("GetPositionsForValue with query %v", query))
result := []lsp.Location{}
for _, value := range v.AllValuesFiles() {
pos, err := util.GetPositionOfNode(&value.ValueNode, query)
queryCopy := append([]string{}, query...)
pos, err := util.GetPositionOfNode(&value.ValueNode, queryCopy)
if err != nil {
logger.Error("Error getting position for value", value, query, err)
yaml, _ := value.Values.YAML()
logger.Error(fmt.Sprintf("Error getting position for value in yaml file %s with query %v ", yaml, query), err)
continue
}
result = append(result, lsp.Location{URI: value.URI, Range: lsp.Range{Start: pos, End: pos}})
Expand Down
4 changes: 3 additions & 1 deletion internal/handler/completion_main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,8 @@ func TestCompletionMainSingleLines(t *testing.T) {
{`Test completion on {{ range .Values.ingress.hosts }} {{ .ho^ }} {{ end }}`, []string{"host", "paths"}, []string{}, nil},
{`Test completion on {{ range .Values.ingress.hosts }} {{ range .paths }} {{ .^ }} {{ end }} {{ end }}`, []string{"pathType", "path"}, []string{}, nil},
{`Test completion on {{ root := . }} {{ $root.test.^ }}`, []string{}, []string{}, errors.New("[$root test ] is no valid template context for helm")},
{`Test completion on {{ range $type, $config := $.Values.deployments }} {{ .^ }} {{ end }}`, []string{"some"}, []string{}, nil},
{`Test completion on {{ range $type, $config := $.Values.deployments }} {{ .s^ }} {{ end }}`, []string{"some"}, []string{}, nil},
}

for _, tt := range testCases {
Expand All @@ -200,10 +202,10 @@ func TestCompletionMainSingleLines(t *testing.T) {
for _, item := range result.Items {
insertTexts = append(insertTexts, item.InsertText)
}

for _, expectedInsertText := range tt.expectedInsertTexts {
assert.Contains(t, insertTexts, expectedInsertText)
}

for _, notExpectedInsertText := range tt.notExpectedInsertTexts {
assert.NotContains(t, insertTexts, notExpectedInsertText)
}
Expand Down
9 changes: 9 additions & 0 deletions internal/handler/hover_main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,15 @@ func TestHoverMain(t *testing.T) {
expected string
expectedError error
}{
{
desc: "Test hover on template context in range over mapping",
position: lsp.Position{
Line: 85,
Character: 26,
},
expected: fmt.Sprintf("### %s\n%s\n\n", filepath.Join("..", "..", "testdata", "example", "values.yaml"), "value"),
expectedError: nil,
},
{
desc: "Test hover on template context with variables",
position: lsp.Position{
Expand Down
7 changes: 7 additions & 0 deletions internal/lsp/document.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package lsp

import (
"bytes"
"fmt"
"strings"

"github.com/mrjosh/helm-ls/internal/util"
Expand All @@ -24,6 +25,12 @@ type Document struct {

// ApplyChanges updates the content of the document from LSP textDocument/didChange events.
func (d *Document) ApplyChanges(changes []lsp.TextDocumentContentChangeEvent) {
defer func() {
if r := recover(); r != nil {
logger.Error(fmt.Sprintf("Recovered in ApplyChanges for %s, the document may be corrupted ", d.URI), r)
}
}()

content := []byte(d.Content)
for _, change := range changes {
start, end := util.PositionToIndex(change.Range.Start, content), util.PositionToIndex(change.Range.End, content)
Expand Down
7 changes: 6 additions & 1 deletion internal/lsp/symbol_table.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ func (t TemplateContext) Format() string {
return strings.Join(t, ".")
}

func (t TemplateContext) Copy() TemplateContext {
return append(TemplateContext{}, t...)
}

func (t TemplateContext) Tail() TemplateContext {
return t[1:]
}
Expand Down Expand Up @@ -65,7 +69,8 @@ func (s *SymbolTable) GetTemplateContext(pointRange sitter.Range) (TemplateConte
if !ok {
return result, fmt.Errorf("no template context found")
}
return result, nil
// return a copy to never modify the original
return result.Copy(), nil
}

func (s *SymbolTable) AddIncludeDefinition(symbol string, pointRange sitter.Range) {
Expand Down
9 changes: 9 additions & 0 deletions internal/lsp/symbol_table_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,15 @@ func TestSymbolTableForValuesSingleTests(t *testing.T) {
},
foundContextsLen: 2,
},
{
template: `{{- range $type, $config := .Values.deployments }} {{ .test }} {{ end }} `,
path: []string{"Values", "deployments[]", "test"},
startPoint: sitter.Point{
Row: 0,
Column: 55,
},
foundContextsLen: 3,
},
}

for _, v := range testCases {
Expand Down
51 changes: 39 additions & 12 deletions internal/util/values.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ func valuesLookup(values chartutil.Values, splittedVar []string) (chartutil.Valu
return chartutil.Values{}, chartutil.ErrNoTable{Key: splittedVar[0]}
}

// PathValue takes a path that traverses a YAML structure and returns the value at the end of that path.
// pathLookup takes a path that traverses a YAML structure and returns the value at the end of that path.
// The path starts at the root of the YAML structure and is comprised of YAML keys separated by periods.
// Given the following YAML data the value at path "chapter.one.title" is "Loomings". The path can also
// include array indexes as in "chapters[].title" which will use the first element of the array.
Expand All @@ -77,7 +77,7 @@ func pathLookup(v chartutil.Values, path []string) (interface{}, error) {
return v, nil
}
if strings.HasSuffix(path[0], "[]") {
return arrayLookup(v, path)
return rangePathLookup(v, path)
}
// if exists must be root key not table
value, ok := v[path[0]]
Expand All @@ -93,32 +93,59 @@ func pathLookup(v chartutil.Values, path []string) (interface{}, error) {
return nil, chartutil.ErrNoTable{Key: path[0]}
}

func arrayLookup(v chartutil.Values, path []string) (interface{}, error) {
func rangePathLookup(v chartutil.Values, path []string) (interface{}, error) {
v2, ok := v[path[0][:(len(path[0])-2)]]
if !ok {
return v, chartutil.ErrNoTable{Key: fmt.Sprintf("Yaml key %s does not exist", path[0])}
}
if v3, ok := v2.([]interface{}); ok {
if len(v3) == 0 {
return chartutil.Values{}, ErrEmpytArray{path[0]}
}
return rangeArrayLookup(v3, path)
}
if nestedValues, ok := v2.(map[string]interface{}); ok {
return rangeMappingLookup(nestedValues, path)
}

return chartutil.Values{}, chartutil.ErrNoTable{Key: path[0]}
}

func rangeArrayLookup(v3 []interface{}, path []string) (interface{}, error) {
if len(v3) == 0 {
return chartutil.Values{}, ErrEmpytArray{path[0]}
}
if len(path) == 1 {
return v3[0], nil
}
if vv, ok := v3[0].(map[string]interface{}); ok {
return pathLookup(vv, path[1:])
}
return chartutil.Values{}, chartutil.ErrNoTable{Key: path[0]}
}

func rangeMappingLookup(nestedValues map[string]interface{}, path []string) (interface{}, error) {
if len(nestedValues) == 0 {
return chartutil.Values{}, ErrEmpytMapping{path[0]}
}

for k := range nestedValues {
if len(path) == 1 {
return v3[0], nil
return nestedValues[k], nil
}
if vv, ok := v3[0].(map[string]interface{}); ok {
return pathLookup(vv, path[1:])
if nestedValues, ok := (nestedValues[k]).(map[string]interface{}); ok {
return pathLookup(nestedValues, path[1:])
}
return chartutil.Values{}, chartutil.ErrNoTable{Key: path[0]}
}

return chartutil.Values{}, chartutil.ErrNoTable{Key: path[0]}
}

type ErrEmpytArray struct {
Key string
}
type ErrEmpytMapping struct {
Key string
}

func (e ErrEmpytArray) Error() string { return fmt.Sprintf("%q is an empyt array", e.Key) }
func (e ErrEmpytArray) Error() string { return fmt.Sprintf("%q is an empyt array", e.Key) }
func (e ErrEmpytMapping) Error() string { return fmt.Sprintf("%q is an empyt mapping", e.Key) }

func builCompletionItem(value interface{}, variable string) lsp.CompletionItem {
var (
Expand Down
13 changes: 13 additions & 0 deletions internal/util/values_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,16 @@ func TestValuesListNested(t *testing.T) {
assert.NoError(t, err)
assert.Equal(t, "1", result)
}

func TestValuesRangeLookupOnMapping(t *testing.T) {
doubleNested := map[string]interface{}{"a": 1}
nested := map[string]interface{}{"nested": doubleNested, "other": doubleNested}
values := map[string]interface{}{"global": nested}

input := []string{"global[]"}
inputCopy := append([]string{}, input...)
result, err := GetTableOrValueForSelector(values, input)
assert.NoError(t, err)
assert.Equal(t, "a: 1\n", result)
assert.Equal(t, inputCopy, input)
}
47 changes: 38 additions & 9 deletions internal/util/yaml.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,36 +14,65 @@ func GetPositionOfNode(node *yamlv3.Node, query []string) (lsp.Position, error)
return lsp.Position{}, fmt.Errorf("could not find Position of %s in values.yaml. Node was zero", query)
}

if node.Kind == yamlv3.DocumentNode {
if len(node.Content) < 1 {
return lsp.Position{}, fmt.Errorf("could not find Position of %s in values.yaml. Document is empty", query)
}
return GetPositionOfNode(node.Content[0], query)
}

if len(query) == 0 {
return lsp.Position{Line: uint32(node.Line) - 1, Character: uint32(node.Column) - 1}, nil
}

query[0] = strings.TrimSuffix(query[0], "[]")
isRange := false

switch node.Kind {
case yamlv3.DocumentNode:
if len(node.Content) < 1 {
return lsp.Position{}, fmt.Errorf("could not find Position of %s in values.yaml. Document is empty", query)
}
return GetPositionOfNode(node.Content[0], query)
if strings.HasSuffix(query[0], "[]") {
query = append([]string{}, query...)
query[0] = strings.TrimSuffix(query[0], "[]")
isRange = true
}

kind := node.Kind
switch kind {
case yamlv3.SequenceNode:
if len(node.Content) > 0 {
return GetPositionOfNode(node.Content[0], query)
}
}

checkNested := []string{}
for index, nestedNode := range node.Content {
checkNested = append(checkNested, nestedNode.Value)
if nestedNode.Value == query[0] {
if len(query) == 1 {
return GetPositionOfNode(nestedNode, query[1:])
}
if len(node.Content) < index+1 {
return lsp.Position{}, fmt.Errorf("could not find Position of %s in values.yaml", query)
return lsp.Position{}, fmt.Errorf("could not find Position of %s in values", query)
}
if isRange {
return getPositionOfNodeAfterRange(node.Content[index+1], query[1:])
}
return GetPositionOfNode(node.Content[index+1], query[1:])
}
}
return lsp.Position{}, fmt.Errorf("could not find Position of %s in values.yaml. Found no match", query)
return lsp.Position{}, fmt.Errorf("could not find Position of %s in values.yaml. Found no match. Possible values %v. Kind is %d", query, checkNested, kind)
}

func getPositionOfNodeAfterRange(node *yamlv3.Node, query []string) (lsp.Position, error) {
switch node.Kind {
case yamlv3.SequenceNode:
if len(node.Content) > 0 {
return GetPositionOfNode(node.Content[0], query)
}
case yamlv3.MappingNode:
if len(node.Content) > 1 {
return GetPositionOfNode(node.Content[1], query)
}
}

return lsp.Position{}, fmt.Errorf("could not find Position of %s in values. Found no match", query)
}

// ReadYamlFileToNode will parse a YAML file into a yaml Node.
Expand Down
Loading
Loading