diff --git a/internal/handler/completion.go b/internal/handler/completion.go index 3026ec0d..191feee7 100644 --- a/internal/handler/completion.go +++ b/internal/handler/completion.go @@ -10,6 +10,7 @@ import ( "strings" lsplocal "github.com/mrjosh/helm-ls/internal/lsp" + gotemplate "github.com/mrjosh/helm-ls/internal/tree-sitter/gotemplate" "github.com/mrjosh/helm-ls/pkg/chartutil" sitter "github.com/smacker/go-tree-sitter" "go.lsp.dev/jsonrpc2" @@ -17,14 +18,6 @@ import ( yaml "gopkg.in/yaml.v2" ) -type ChildNodeType string - -const ( - ChildNodeTypeIdentifier = "identifier" - ChildNodeTypeDot = "dot" - ChildNodeTypeDotSymbol = "." -) - var ( emptyItems = make([]lsp.CompletionItem, 0) functionsCompletionItems = make([]lsp.CompletionItem, 0) @@ -107,7 +100,7 @@ func completionAstParsing(doc *lsplocal.Document, position lsp.Position) string Row: position.Line, Column: position.Character, } - relevantChildNode = findRelevantChildNode(currentNode, pointToLoopUp) + relevantChildNode = lsplocal.FindRelevantChildNode(currentNode, pointToLoopUp) word string ) @@ -115,12 +108,12 @@ func completionAstParsing(doc *lsplocal.Document, position lsp.Position) string logger.Println("relevantChildNode", relevantChildNode.Type()) switch relevantChildNode.Type() { - case ChildNodeTypeIdentifier: + case gotemplate.NodeTypeIdentifier: word = relevantChildNode.Content([]byte(doc.Content)) - case ChildNodeTypeDot: + case gotemplate.NodeTypeDot: logger.Println("TraverseIdentifierPathUp") word = lsplocal.TraverseIdentifierPathUp(relevantChildNode, doc) - case ChildNodeTypeDotSymbol: + case gotemplate.NodeTypeDotSymbol: logger.Println("GetFieldIdentifierPath") word = lsplocal.GetFieldIdentifierPath(relevantChildNode, doc) } @@ -128,24 +121,6 @@ func completionAstParsing(doc *lsplocal.Document, position lsp.Position) string return word } -func findRelevantChildNode(currentNode *sitter.Node, pointToLookUp sitter.Point) *sitter.Node { - for i := 0; i < int(currentNode.ChildCount()); i++ { - child := currentNode.Child(i) - if isPointLargerOrEq(pointToLookUp, child.StartPoint()) && isPointLargerOrEq(child.EndPoint(), pointToLookUp) { - logger.Println("loop", child) - return findRelevantChildNode(child, pointToLookUp) - } - } - return currentNode -} - -func isPointLargerOrEq(a sitter.Point, b sitter.Point) bool { - if a.Row == b.Row { - return a.Column >= b.Column - } - return a.Row > b.Row -} - func (h *langHandler) getValue(values chartutil.Values, splittedVar []string) []lsp.CompletionItem { var ( diff --git a/internal/handler/definition.go b/internal/handler/definition.go index e86ebaeb..34c855ea 100644 --- a/internal/handler/definition.go +++ b/internal/handler/definition.go @@ -3,17 +3,145 @@ package handler import ( "context" "encoding/json" + "errors" + "fmt" + "strings" + lsplocal "github.com/mrjosh/helm-ls/internal/lsp" + gotemplate "github.com/mrjosh/helm-ls/internal/tree-sitter/gotemplate" + "github.com/mrjosh/helm-ls/internal/util" + sitter "github.com/smacker/go-tree-sitter" "go.lsp.dev/jsonrpc2" lsp "go.lsp.dev/protocol" ) -func (h *langHandler) handleDefinition(_ context.Context, _ jsonrpc2.Replier, req jsonrpc2.Request) (err error) { - +func (h *langHandler) handleDefinition(ctx context.Context, reply jsonrpc2.Replier, req jsonrpc2.Request) (err error) { if req.Params() == nil { return &jsonrpc2.Error{Code: jsonrpc2.InvalidParams} } var params lsp.DefinitionParams - return json.Unmarshal(req.Params(), ¶ms) + if err := json.Unmarshal(req.Params(), ¶ms); err != nil { + return err + } + + doc, ok := h.documents.Get(params.TextDocument.URI) + if !ok { + return errors.New("Could not get document: " + params.TextDocument.URI.Filename()) + } + result, err := h.definitionAstParsing(doc, params.Position) + + if err != nil { + // supress errors for clients + // otherwise using go-to-definition on words that have no definition + // will result in an error + logger.Println(err) + return reply(ctx, nil, nil) + } + return reply(ctx, result, err) +} + +func (h *langHandler) definitionAstParsing(doc *lsplocal.Document, position lsp.Position) (lsp.Location, error) { + var ( + currentNode = lsplocal.NodeAtPosition(doc.Ast, position) + pointToLoopUp = sitter.Point{ + Row: position.Line, + Column: position.Character, + } + relevantChildNode = lsplocal.FindRelevantChildNode(currentNode, pointToLoopUp) + ) + + switch relevantChildNode.Type() { + case gotemplate.NodeTypeIdentifier: + if relevantChildNode.Parent().Type() == gotemplate.NodeTypeVariable { + return h.getDefinitionForVariable(relevantChildNode, doc) + } + return h.getDefinitionForFixedIdentifier(relevantChildNode, doc) + case gotemplate.NodeTypeDot, gotemplate.NodeTypeDotSymbol, gotemplate.NodeTypeFieldIdentifier: + return h.getDefinitionForValue(relevantChildNode, doc) + } + + return lsp.Location{}, fmt.Errorf("Definition not implemented for node type %s", relevantChildNode.Type()) +} + +func (h *langHandler) getDefinitionForVariable(node *sitter.Node, doc *lsplocal.Document) (lsp.Location, error) { + variableName := node.Content([]byte(doc.Content)) + var defintionNode = lsplocal.GetVariableDefinition(variableName, node.Parent(), doc.Content) + if defintionNode == nil { + return lsp.Location{}, fmt.Errorf("Could not find definition for %s", variableName) + } + return lsp.Location{URI: doc.URI, Range: lsp.Range{Start: util.PointToPosition(defintionNode.StartPoint())}}, nil +} + +// getDefinitionForFixedIdentifier checks if the current identifier has a constant definition and returns it +func (h *langHandler) getDefinitionForFixedIdentifier(node *sitter.Node, doc *lsplocal.Document) (lsp.Location, error) { + var name = node.Content([]byte(doc.Content)) + switch name { + case "Values": + return lsp.Location{ + URI: h.projectFiles.GetValuesFileURI()}, nil + case "Chart": + return lsp.Location{ + URI: h.projectFiles.GetChartFileURI()}, nil + } + + return lsp.Location{}, fmt.Errorf("Could not find definition for %s", name) +} + +func (h *langHandler) getDefinitionForValue(node *sitter.Node, doc *lsplocal.Document) (lsp.Location, error) { + var ( + yamlPathString = getYamlPath(node, doc) + yamlPath, err = util.NewYamlPath(yamlPathString) + definitionFileURI lsp.DocumentURI + position lsp.Position + ) + if err != nil { + return lsp.Location{}, err + } + + if yamlPath.IsValuesPath() { + definitionFileURI = h.projectFiles.GetValuesFileURI() + position, err = h.getValueDefinition(yamlPath.GetTail()) + } + if yamlPath.IsChartPath() { + definitionFileURI = h.projectFiles.GetChartFileURI() + position, err = h.getChartDefinition(yamlPath.GetTail()) + } + + if err == nil && definitionFileURI != "" { + return lsp.Location{ + URI: definitionFileURI, + Range: lsp.Range{Start: position}, + }, nil + } + return lsp.Location{}, fmt.Errorf("Could not find definition for %s", yamlPath) +} + +func getYamlPath(node *sitter.Node, doc *lsplocal.Document) string { + switch node.Type() { + case gotemplate.NodeTypeDot: + return lsplocal.TraverseIdentifierPathUp(node, doc) + case gotemplate.NodeTypeDotSymbol, gotemplate.NodeTypeFieldIdentifier: + return lsplocal.GetFieldIdentifierPath(node, doc) + default: + return "" + } +} + +func (h *langHandler) getValueDefinition(splittedVar []string) (lsp.Position, error) { + return util.GetPositionOfNode(h.valueNode, splittedVar) +} + +func (h *langHandler) getChartDefinition(splittedVar []string) (lsp.Position, error) { + modifyedVar := make([]string, 0) + // for Charts, we make the first letter lowercase + for _, value := range splittedVar { + restOfString := "" + if (len(value)) > 1 { + restOfString = value[1:] + } + firstLetterLowercase := strings.ToLower(string(value[0])) + restOfString + modifyedVar = append(modifyedVar, firstLetterLowercase) + } + return util.GetPositionOfNode(h.chartNode, modifyedVar) } diff --git a/internal/handler/definition_test.go b/internal/handler/definition_test.go new file mode 100644 index 00000000..9b9ab57b --- /dev/null +++ b/internal/handler/definition_test.go @@ -0,0 +1,157 @@ +package handler + +import ( + "context" + "fmt" + "reflect" + "testing" + + lsplocal "github.com/mrjosh/helm-ls/internal/lsp" + gotemplate "github.com/mrjosh/helm-ls/internal/tree-sitter/gotemplate" + sitter "github.com/smacker/go-tree-sitter" + lsp "go.lsp.dev/protocol" + "go.lsp.dev/uri" + yamlv3 "gopkg.in/yaml.v3" +) + +var testFileContent = ` +{{ $variable := "text" }} # line 1 +{{ $variable }} # line 2 + +{{ $someOther := "text" }}# line 4 +{{ $variable }} # line 5 + +{{ range $index, $element := pipeline }}{{ $index }}{{ $element }}{{ end }} # line 7 +{{ .Values.foo }} # line 8 +{{ .Values.something.nested }} # line 9 +` + +var testDocumentTemplateURI = uri.URI("file:///test.yaml") +var testValuesURI = uri.URI("file:///values.yaml") +var valuesContent = ` +foo: bar +something: + nested: false +` + +func genericDefinitionTest(t *testing.T, position lsp.Position, expectedLocation lsp.Location, expectedError error) { + var node yamlv3.Node + var err = yamlv3.Unmarshal([]byte(valuesContent), &node) + if err != nil { + t.Fatal(err) + } + handler := &langHandler{ + linterName: "helm-lint", + connPool: nil, + documents: nil, + values: make(map[string]interface{}), + valueNode: node, + projectFiles: ProjectFiles{ + ValuesFile: "/values.yaml", + ChartFile: "", + }, + } + + parser := sitter.NewParser() + parser.SetLanguage(gotemplate.GetLanguage()) + tree, _ := parser.ParseCtx(context.Background(), nil, []byte(testFileContent)) + doc := &lsplocal.Document{ + Content: testFileContent, + URI: testDocumentTemplateURI, + Ast: tree, + } + + location, err := handler.definitionAstParsing(doc, position) + + if err != nil && err.Error() != expectedError.Error() { + t.Errorf("expected %v, got %v", expectedError, err) + } + + if reflect.DeepEqual(location, expectedLocation) == false { + t.Errorf("expected %v, got %v", expectedLocation, location) + } +} + +// Input: +// {{ $variable }} # line 2 +// -----| # this line incides the coursor position for the test +func TestDefinitionVariable(t *testing.T) { + genericDefinitionTest(t, lsp.Position{Line: 2, Character: 8}, lsp.Location{ + URI: testDocumentTemplateURI, + Range: lsp.Range{ + Start: lsp.Position{ + Line: 1, + Character: 3, + }, + }, + }, nil) +} + +func TestDefinitionNotImplemented(t *testing.T) { + genericDefinitionTest(t, lsp.Position{Line: 1, Character: 1}, lsp.Location{ + Range: lsp.Range{}, + }, + fmt.Errorf("Definition not implemented for node type %s", "{{")) +} + +// Input: +// {{ range $index, $element := pipeline }}{{ $index }}{{ $element }}{{ end }} # line 7 +// -----------------------------------------------------------| +// Expected: +// {{ range $index, $element := pipeline }}{{ $index }}{{ $element }}{{ end }} # line 7 +// -----------------| +func TestDefinitionRange(t *testing.T) { + genericDefinitionTest(t, lsp.Position{Line: 7, Character: 60}, lsp.Location{ + URI: testDocumentTemplateURI, + Range: lsp.Range{ + Start: lsp.Position{ + Line: 7, + Character: 17, + }, + }, + }, nil) +} + +// Input: +// {{ .Values.foo }} # line 8 +// ------------| +func TestDefinitionValue(t *testing.T) { + genericDefinitionTest(t, lsp.Position{Line: 8, Character: 13}, lsp.Location{ + URI: testValuesURI, + Range: lsp.Range{ + Start: lsp.Position{ + Line: 1, + Character: 0, + }, + }, + }, nil) +} + +// Input: +// {{ .Values.something.nested }} # line 9 +// ----------------------| +func TestDefinitionValueNested(t *testing.T) { + genericDefinitionTest(t, lsp.Position{Line: 9, Character: 26}, lsp.Location{ + URI: testValuesURI, + Range: lsp.Range{ + Start: lsp.Position{ + Line: 3, + Character: 2, + }, + }, + }, nil) +} + +// {{ .Values.foo }} # line 8 +// ------| +func TestDefinitionValueFile(t *testing.T) { + genericDefinitionTest(t, lsp.Position{Line: 8, Character: 7}, lsp.Location{ + URI: testValuesURI, + Range: lsp.Range{ + Start: lsp.Position{ + Line: 0, + Character: 0, + }, + }, + }, nil) +} diff --git a/internal/handler/handler.go b/internal/handler/handler.go index 205c8a68..a74dc289 100644 --- a/internal/handler/handler.go +++ b/internal/handler/handler.go @@ -4,8 +4,6 @@ import ( "context" "encoding/json" "errors" - "net/url" - "path/filepath" "github.com/mrjosh/helm-ls/internal/adapter/fs" lsplocal "github.com/mrjosh/helm-ls/internal/lsp" @@ -13,6 +11,8 @@ import ( "github.com/mrjosh/helm-ls/pkg/chartutil" "go.lsp.dev/jsonrpc2" lsp "go.lsp.dev/protocol" + "go.lsp.dev/uri" + yamlv3 "gopkg.in/yaml.v3" "github.com/mrjosh/helm-ls/internal/log" ) @@ -23,17 +23,23 @@ type langHandler struct { connPool jsonrpc2.Conn linterName string documents *lsplocal.DocumentStore + projectFiles ProjectFiles values chartutil.Values chartMetadata chart.Metadata + valueNode yamlv3.Node + chartNode yamlv3.Node } func NewHandler(connPool jsonrpc2.Conn) jsonrpc2.Handler { fileStorage, _ := fs.NewFileStorage("") handler := &langHandler{ - linterName: "helm-lint", - connPool: connPool, - documents: lsplocal.NewDocumentStore(fileStorage), - values: make(map[string]interface{}), + linterName: "helm-lint", + connPool: connPool, + documents: lsplocal.NewDocumentStore(fileStorage), + projectFiles: ProjectFiles{}, + values: make(map[string]interface{}), + valueNode: yamlv3.Node{}, + chartNode: yamlv3.Node{}, } logger.Printf("helm-lint-langserver: connections opened") return jsonrpc2.ReplyHandler(handler.handle) @@ -79,26 +85,37 @@ func (h *langHandler) handleInitialize(ctx context.Context, reply jsonrpc2.Repli return errors.New("length WorkspaceFolders is 0") } - workspace_uri, err := url.Parse(params.WorkspaceFolders[0].URI) + workspaceURI, err := uri.Parse(params.WorkspaceFolders[0].URI) if err != nil { return err } - vf := filepath.Join(workspace_uri.Path, "values.yaml") - vals, err := chartutil.ReadValuesFile(vf) + h.projectFiles = NewProjectFiles(workspaceURI, "") + + vals, err := chartutil.ReadValuesFile(h.projectFiles.ValuesFile) if err != nil { logger.Println("Error loading values.yaml file", err) return err } h.values = vals - chartFile := filepath.Join(workspace_uri.Path, "Chart.yaml") - chartMetadata, err := chartutil.LoadChartfile(chartFile) + chartMetadata, err := chartutil.LoadChartfile(h.projectFiles.ChartFile) if err != nil { logger.Println("Error loading Chart.yaml file", err) return err } h.chartMetadata = *chartMetadata + valueNodes, err := chartutil.ReadYamlFileToNode(h.projectFiles.ValuesFile) + if err != nil { + return err + } + h.valueNode = valueNodes + + chartNode, err := chartutil.ReadYamlFileToNode(h.projectFiles.ChartFile) + if err != nil { + return err + } + h.chartNode = chartNode return reply(ctx, lsp.InitializeResult{ Capabilities: lsp.ServerCapabilities{ @@ -113,7 +130,8 @@ func (h *langHandler) handleInitialize(ctx context.Context, reply jsonrpc2.Repli TriggerCharacters: []string{".", "$."}, ResolveProvider: false, }, - HoverProvider: true, + HoverProvider: true, + DefinitionProvider: true, }, }, nil) } diff --git a/internal/handler/project_files.go b/internal/handler/project_files.go new file mode 100644 index 00000000..7b2420f6 --- /dev/null +++ b/internal/handler/project_files.go @@ -0,0 +1,39 @@ +package handler + +import ( + "errors" + "os" + "path/filepath" + + "github.com/mrjosh/helm-ls/pkg/chartutil" + lsp "go.lsp.dev/protocol" + "go.lsp.dev/uri" +) + +type ProjectFiles struct { + ValuesFile string + ChartFile string +} + +func (p ProjectFiles) GetValuesFileURI() lsp.DocumentURI { + return "file://" + lsp.DocumentURI(p.ValuesFile) +} +func (p ProjectFiles) GetChartFileURI() lsp.DocumentURI { + return "file://" + lsp.DocumentURI(p.ChartFile) +} + +func NewProjectFiles(rootURI uri.URI, valuesFileName string) ProjectFiles { + + if valuesFileName == "" { + valuesFileName = chartutil.ValuesfileName + } + valuesFileName = filepath.Join(rootURI.Filename(), valuesFileName) + if _, err := os.Stat(valuesFileName); errors.Is(err, os.ErrNotExist) { + valuesFileName = filepath.Join(rootURI.Filename(), "values.yml") + } + + return ProjectFiles{ + ValuesFile: valuesFileName, + ChartFile: filepath.Join(rootURI.Filename(), chartutil.ChartfileName), + } +} diff --git a/internal/lsp/ast.go b/internal/lsp/ast.go index 426b26d8..2de46ebf 100644 --- a/internal/lsp/ast.go +++ b/internal/lsp/ast.go @@ -20,14 +20,30 @@ func NodeAtPosition(tree *sitter.Tree, position lsp.Position) *sitter.Node { return tree.RootNode().NamedDescendantForPointRange(start, start) } +func FindRelevantChildNode(currentNode *sitter.Node, pointToLookUp sitter.Point) *sitter.Node { + for i := 0; i < int(currentNode.ChildCount()); i++ { + child := currentNode.Child(i) + if isPointLargerOrEq(pointToLookUp, child.StartPoint()) && isPointLargerOrEq(child.EndPoint(), pointToLookUp) { + return FindRelevantChildNode(child, pointToLookUp) + } + } + return currentNode +} + +func isPointLargerOrEq(a sitter.Point, b sitter.Point) bool { + if a.Row == b.Row { + return a.Column >= b.Column + } + return a.Row > b.Row +} + func GetFieldIdentifierPath(node *sitter.Node, doc *Document) (path string) { path = buildFieldIdentifierPath(node, doc) - logger.Println("buildFieldIdentifierPath:", path) + logger.Debug("buildFieldIdentifierPath:", path) return path } func buildFieldIdentifierPath(node *sitter.Node, doc *Document) string { - prepend := node.PrevNamedSibling() currentPath := node.Content([]byte(doc.Content)) @@ -62,13 +78,13 @@ func TraverseIdentifierPathUp(node *sitter.Node, doc *Document) string { if node.PrevNamedSibling() == nil { return TraverseIdentifierPathUp(parent, doc) } - logger.Println("Range action found ") + logger.Debug("Range action found") return TraverseIdentifierPathUp(parent, doc) + parent.NamedChild(0).Content([]byte(doc.Content)) + "[0]" case "with_action": if node.PrevNamedSibling() == nil { return TraverseIdentifierPathUp(parent, doc) } - logger.Println("With action found") + logger.Debug("With action found") return TraverseIdentifierPathUp(parent, doc) + parent.NamedChild(0).Content([]byte(doc.Content)) } return TraverseIdentifierPathUp(parent, doc) diff --git a/internal/lsp/ast_variable.go b/internal/lsp/ast_variable.go new file mode 100644 index 00000000..696c9540 --- /dev/null +++ b/internal/lsp/ast_variable.go @@ -0,0 +1,54 @@ +package lsp + +import ( + "github.com/mrjosh/helm-ls/internal/tree-sitter/gotemplate" + sitter "github.com/smacker/go-tree-sitter" +) + +func GetVariableDefinitionOfNode(node *sitter.Node, template string) *sitter.Node { + if node == nil { + return nil + } + if node.Type() != gotemplate.NodeTypeVariable { + return nil + } + variableName := node.Child(1).Content([]byte(template)) + + return GetVariableDefinition(variableName, node, template) +} + +func GetVariableDefinition(variableName string, node *sitter.Node, template string) *sitter.Node { + if node == nil { + return nil + } + + logger.Debug("GetVariableDefinition:", node.Type(), variableName) + + switch node.Type() { + case gotemplate.NodeTypeRangeVariableDefinition: + var ( + indexDefinitionNode = node.ChildByFieldName("index") + elementDefinitionNode = node.ChildByFieldName("element") + indexDefinitionName = indexDefinitionNode.ChildByFieldName("name").Content([]byte(template)) + elementDefinitionName = elementDefinitionNode.ChildByFieldName("name").Content([]byte(template)) + ) + if indexDefinitionName == variableName { + return indexDefinitionNode + } + if elementDefinitionName == variableName { + return elementDefinitionNode + } + case gotemplate.NodeTypeVariableDefinition: + currentVariableName := node.ChildByFieldName("variable").Child(1).Content([]byte(template)) + logger.Debug("currentVariableName:", currentVariableName) + logger.Debug("variableName:", variableName) + if currentVariableName == variableName { + return node + } + } + nextNode := node.PrevNamedSibling() + if nextNode == nil { + nextNode = node.Parent() + } + return GetVariableDefinition(variableName, nextNode, template) +} diff --git a/internal/lsp/ast_variable_test.go b/internal/lsp/ast_variable_test.go new file mode 100644 index 00000000..5a085811 --- /dev/null +++ b/internal/lsp/ast_variable_test.go @@ -0,0 +1,88 @@ +package lsp + +import ( + "context" + "testing" + + "github.com/mrjosh/helm-ls/internal/tree-sitter/gotemplate" + sitter "github.com/smacker/go-tree-sitter" +) + +func TestGetVariableDefinitionDirectDecleration(t *testing.T) { + + var template = ` +{{ $variable := "text" }} +{{ $variable }} + ` + + node, err := sitter.ParseCtx(context.Background(), []byte(template), gotemplate.GetLanguage()) + + if err != nil { + t.Errorf("Parsing did not work") + } + + usageNode := node.NamedChild(3) + definitionNode := GetVariableDefinitionOfNode(usageNode, template) + + if definitionNode == nil { + t.Errorf("Could not get definitionNode") + } else if definitionNode.Content([]byte(template)) != "$variable := \"text\"" { + t.Errorf("Definition did not match but was %s", definitionNode.Content([]byte(template))) + } +} + +func TestGetVariableDefinitionOtherDecleration(t *testing.T) { + var template = ` +{{ $variable := "text" }} +{{ $someOther := "text" }} +{{ $variable }} + ` + + node, err := sitter.ParseCtx(context.Background(), []byte(template), gotemplate.GetLanguage()) + + if err != nil { + t.Errorf("Parsing did not work") + } + + usageNode := node.NamedChild(5) + definitionNode := GetVariableDefinitionOfNode(usageNode, template) + + if definitionNode == nil { + t.Errorf("Could not get definitionNode") + } else if definitionNode.Content([]byte(template)) != "$variable := \"text\"" { + t.Errorf("Definition did not match but was %s", definitionNode.Content([]byte(template))) + } + +} + +func TestGetVariableDefinitionRange(t *testing.T) { + var template = ` +{{ range $index, $element := pipeline }}{{ $index }}{{ $element }}{{ end }} + ` + + node, err := sitter.ParseCtx(context.Background(), []byte(template), gotemplate.GetLanguage()) + + if err != nil { + t.Errorf("Parsing did not work") + } + + elementUsageNode := node.NamedChild(1).NamedChild(2) + logger.Println(elementUsageNode.Content([]byte(template))) // $element + definitionNode := GetVariableDefinitionOfNode(elementUsageNode, template) + + if definitionNode == nil { + t.Errorf("Could not get definitionNode") + } else if definitionNode.Content([]byte(template)) != "$element" { + t.Errorf("Definition did not match but was %s", definitionNode.Content([]byte(template))) + } + + indexUsageNode := node.NamedChild(1).NamedChild(1) + logger.Println(elementUsageNode.Content([]byte(template))) // $index + definitionNode = GetVariableDefinitionOfNode(indexUsageNode, template) + + if definitionNode == nil { + t.Errorf("Could not get definitionNode") + } else if definitionNode.Content([]byte(template)) != "$index" { + t.Errorf("Definition did not match but was %s", definitionNode.Content([]byte(template))) + } +} diff --git a/internal/tree-sitter/gotemplate/node-types.go b/internal/tree-sitter/gotemplate/node-types.go new file mode 100644 index 00000000..25bdcf91 --- /dev/null +++ b/internal/tree-sitter/gotemplate/node-types.go @@ -0,0 +1,12 @@ +package gotemplate + +const ( + NodeTypeIdentifier = "identifier" + NodeTypeVariable = "variable" + NodeTypeDot = "dot" + NodeTypeDollar = "$" + NodeTypeDotSymbol = "." + NodeTypeVariableDefinition = "variable_definition" + NodeTypeRangeVariableDefinition = "range_variable_definition" + NodeTypeFieldIdentifier = "field_identifier" +) diff --git a/internal/util/points.go b/internal/util/points.go new file mode 100644 index 00000000..1e789c1d --- /dev/null +++ b/internal/util/points.go @@ -0,0 +1,14 @@ +package util + +import ( + sitter "github.com/smacker/go-tree-sitter" + lsp "go.lsp.dev/protocol" +) + +func PointToPosition(point sitter.Point) lsp.Position { + return lsp.Position{Line: point.Row, Character: point.Column} +} + +func PositionToPoint(position lsp.Position) sitter.Point { + return sitter.Point{Row: position.Line, Column: position.Character} +} diff --git a/internal/util/yaml.go b/internal/util/yaml.go new file mode 100644 index 00000000..140b6ecd --- /dev/null +++ b/internal/util/yaml.go @@ -0,0 +1,34 @@ +package util + +import ( + "fmt" + + lsp "go.lsp.dev/protocol" + yamlv3 "gopkg.in/yaml.v3" +) + +func GetPositionOfNode(node yamlv3.Node, query []string) (lsp.Position, error) { + if node.IsZero() { + return lsp.Position{}, fmt.Errorf("could not find Position of %s in values.yaml. Node was zero", query) + } + + for index, value := range node.Content { + if value.Value == "" { + result, err := GetPositionOfNode(*value, query) + if err == nil { + return result, nil + } + } + if value.Value == query[0] { + if len(query) > 1 { + if len(node.Content) < index+1 { + return lsp.Position{}, fmt.Errorf("could not find Position of %s in values.yaml", query) + } + return GetPositionOfNode(*node.Content[index+1], query[1:]) + } + return lsp.Position{Line: uint32(value.Line) - 1, Character: uint32(value.Column) - 1}, nil + } + } + return lsp.Position{}, fmt.Errorf("could not find Position of %s in values.yaml. Found no match", query) + +} diff --git a/internal/util/yaml_path.go b/internal/util/yaml_path.go new file mode 100644 index 00000000..d07c4e84 --- /dev/null +++ b/internal/util/yaml_path.go @@ -0,0 +1,59 @@ +package util + +import ( + "fmt" + "strings" +) + +type YamlPath struct { + TableNames []string +} + +func NewYamlPath(yamlPathString string) (YamlPath, error) { + var ( + splitted = strings.Split(yamlPathString, ".") + variableSplitted = []string{} + ) + + // filter out empty strings, that were added by the split + for _, s := range splitted { + if s != "" { + variableSplitted = append(variableSplitted, s) + } + } + if len(variableSplitted) == 0 { + return YamlPath{}, fmt.Errorf("Could not parse yaml path: %s", yamlPathString) + } + // $ always points to the root context so we can safely remove it + // as long the LSP does not know about ranges + if variableSplitted[0] == "$" && len(variableSplitted) > 1 { + variableSplitted = variableSplitted[1:] + } + + return YamlPath{ + TableNames: variableSplitted, + }, nil +} + +func (path YamlPath) GetTail() []string { + return path.TableNames[1:] +} + +func (path YamlPath) IsValuesPath() bool { + return path.TableNames[0] == "Values" +} + +func (path YamlPath) IsChartPath() bool { + return path.TableNames[0] == "Chart" +} + +func (path YamlPath) IsReleasePath() bool { + return path.TableNames[0] == "Release" +} + +func (path YamlPath) IsFilesPath() bool { + return path.TableNames[0] == "Files" +} +func (path YamlPath) IsCapabilitiesPath() bool { + return path.TableNames[0] == "Capabilities" +} diff --git a/internal/util/yaml_test.go b/internal/util/yaml_test.go new file mode 100644 index 00000000..01b3a867 --- /dev/null +++ b/internal/util/yaml_test.go @@ -0,0 +1,54 @@ +package util + +import ( + "fmt" + lsp "go.lsp.dev/protocol" + "os" + "testing" + + "gopkg.in/yaml.v3" +) + +func TestGetPositionOfNode(t *testing.T) { + + data, err := os.ReadFile("./yaml_test_input.yaml") + if err != nil { + print(fmt.Sprint(err)) + t.Errorf("error yml parsing") + } + + var node yaml.Node + err = yaml.Unmarshal(data, &node) + + if err != nil { + print(fmt.Sprint(err)) + t.Errorf("error yml parsing") + } + + result, err := GetPositionOfNode(node, []string{"replicaCount"}) + expected := lsp.Position{Line: 5, Character: 0} + if err != nil { + t.Errorf("Result had error: %s", err) + } + if result != expected { + t.Errorf("Result was not expected Position %v but was %v", expected, result) + } + + result, err = GetPositionOfNode(node, []string{"image", "repository"}) + expected = lsp.Position{Line: 8, Character: 2} + if err != nil { + t.Errorf("Result had error: %s", err) + } + if result != expected { + t.Errorf("Result was not expected Position %v but was %v", expected, result) + } + + result, err = GetPositionOfNode(node, []string{"service", "test", "nested", "value"}) + expected = lsp.Position{Line: 30, Character: 6} + if err != nil { + t.Errorf("Result had error: %s", err) + } + if result != expected { + t.Errorf("Result was not expected Position %v but was %v", expected, result) + } +} diff --git a/internal/util/yaml_test_input.yaml b/internal/util/yaml_test_input.yaml new file mode 100644 index 00000000..81ba0102 --- /dev/null +++ b/internal/util/yaml_test_input.yaml @@ -0,0 +1,33 @@ +--- +global: + some: + test: 1 + +replicaCount: 1 + +image: + repository: nginx + pullPolicy: IfNotPresent + # Overrides the image tag whose default is the chart appVersion. + tag: "" + +nameOverride: "" +fullnameOverride: "" + +serviceAccount: + # Specifies whether a service account should be created + create: true + # Annotations to add to the service account + annotations: {} + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: "" + +service: + type: ClusterIP + port: 80 + test: + nested: + value: test + + diff --git a/pkg/chartutil/values.go b/pkg/chartutil/values.go index 600aac54..192339b5 100644 --- a/pkg/chartutil/values.go +++ b/pkg/chartutil/values.go @@ -23,6 +23,7 @@ import ( "strings" "github.com/pkg/errors" + yamlv3 "gopkg.in/yaml.v3" "sigs.k8s.io/yaml" "github.com/mrjosh/helm-ls/pkg/chart" @@ -152,6 +153,17 @@ func ReadValuesFile(filename string) (Values, error) { return ReadValues(data) } +// ReadYamlFileToNode will parse a YAML file into a yaml Node. +func ReadYamlFileToNode(filename string) (node yamlv3.Node, err error) { + data, err := os.ReadFile(filename) + if err != nil { + return yamlv3.Node{}, err + } + + err = yamlv3.Unmarshal(data, &node) + return node, err +} + // ReleaseOptions represents the additional release options needed // for the composition of the final values struct type ReleaseOptions struct {