From 48acac4dc731f035727a0f791e862240a7a61be1 Mon Sep 17 00:00:00 2001 From: qvalentin Date: Mon, 10 Apr 2023 18:17:53 +0200 Subject: [PATCH 1/8] feat(definition): go to defintion for values and Chart --- internal/handler/definition.go | 86 +++++++++++++++++++++++++++++- internal/handler/handler.go | 23 +++++++- internal/util/yaml.go | 38 +++++++++++++ internal/util/yaml_test.go | 54 +++++++++++++++++++ internal/util/yaml_test_input.yaml | 33 ++++++++++++ pkg/chartutil/values.go | 12 +++++ 6 files changed, 243 insertions(+), 3 deletions(-) create mode 100644 internal/util/yaml.go create mode 100644 internal/util/yaml_test.go create mode 100644 internal/util/yaml_test_input.yaml diff --git a/internal/handler/definition.go b/internal/handler/definition.go index e86ebaeb..28e85ebb 100644 --- a/internal/handler/definition.go +++ b/internal/handler/definition.go @@ -3,17 +3,99 @@ package handler import ( "context" "encoding/json" + "errors" + "fmt" + "path/filepath" + "strings" + "github.com/mrjosh/helm-ls/internal/util" "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) { + logger.Println(fmt.Sprintf("Definition provider")) 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()) + } + + var ( + word = doc.ValueAt(params.Position) + splitted = strings.Split(word, ".") + variableSplitted = []string{} + position lsp.Position + defitionFilePath string + ) + + if word == "" { + return reply(ctx, nil, err) + } + + for _, s := range splitted { + if s != "" { + variableSplitted = append(variableSplitted, s) + } + } + + // $ 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:] + } + + logger.Println(fmt.Sprintf("Definition checking for word < %s >", word)) + + switch variableSplitted[0] { + case "Values": + defitionFilePath = filepath.Join(h.rootURI.Filename(), "values.yaml") + if len(variableSplitted) > 1 { + position, err = h.getValueDefinition(variableSplitted[1:]) + } + case "Chart": + defitionFilePath = filepath.Join(h.rootURI.Filename(), "Chart.yaml") + if len(variableSplitted) > 1 { + position, err = h.getChartDefinition(variableSplitted[1:]) + } + } + + if err == nil && defitionFilePath != "" { + result := lsp.Location{ + URI: "file://" + lsp.DocumentURI(defitionFilePath), + Range: lsp.Range{Start: position}, + } + + return reply(ctx, result, err) + } + logger.Printf("Had no match for definition. Error: %v", err) + return reply(ctx, nil, err) +} + +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 _, 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/handler.go b/internal/handler/handler.go index 205c8a68..de1deec1 100644 --- a/internal/handler/handler.go +++ b/internal/handler/handler.go @@ -13,6 +13,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" ) @@ -25,6 +27,9 @@ type langHandler struct { documents *lsplocal.DocumentStore values chartutil.Values chartMetadata chart.Metadata + valueNode yamlv3.Node + chartNode yamlv3.Node + rootURI uri.URI } func NewHandler(connPool jsonrpc2.Conn) jsonrpc2.Handler { @@ -34,6 +39,9 @@ func NewHandler(connPool jsonrpc2.Conn) jsonrpc2.Handler { connPool: connPool, documents: lsplocal.NewDocumentStore(fileStorage), values: make(map[string]interface{}), + valueNode: yamlv3.Node{}, + chartNode: yamlv3.Node{}, + rootURI: "", } logger.Printf("helm-lint-langserver: connections opened") return jsonrpc2.ReplyHandler(handler.handle) @@ -85,6 +93,7 @@ func (h *langHandler) handleInitialize(ctx context.Context, reply jsonrpc2.Repli } vf := filepath.Join(workspace_uri.Path, "values.yaml") + vals, err := chartutil.ReadValuesFile(vf) if err != nil { logger.Println("Error loading values.yaml file", err) @@ -99,6 +108,17 @@ func (h *langHandler) handleInitialize(ctx context.Context, reply jsonrpc2.Repli return err } h.chartMetadata = *chartMetadata + valueNodes, err := chartutil.ReadYamlFileToNodes(vf) + if err != nil { + return err + } + h.valueNode = valueNodes + + chartNode, err := chartutil.ReadYamlFileToNodes(chartFile) + if err != nil { + return err + } + h.chartNode = chartNode return reply(ctx, lsp.InitializeResult{ Capabilities: lsp.ServerCapabilities{ @@ -113,7 +133,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/util/yaml.go b/internal/util/yaml.go new file mode 100644 index 00000000..5d7d201f --- /dev/null +++ b/internal/util/yaml.go @@ -0,0 +1,38 @@ +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) + } + println(node.Value) + + 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) + } else { + return GetPositionOfNode(*node.Content[index+1], query[1:]) + } + } else { + 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_test.go b/internal/util/yaml_test.go new file mode 100644 index 00000000..af48843a --- /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: 6, Character: 1} + 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: 9, Character: 3} + 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: 31, Character: 7} + 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..1adfde25 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) } +// ReadYamlFileToNodes will parse a YAML file into a yaml Nodes. +func ReadYamlFileToNodes(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 { From 8cc43a8900f356072e89d47ff8b1136791276e15 Mon Sep 17 00:00:00 2001 From: qvalentin Date: Sun, 2 Jul 2023 13:18:04 +0200 Subject: [PATCH 2/8] feat(definition): WIP: go to defintion for variables --- internal/handler/completion.go | 35 ++------- internal/handler/definition.go | 49 +++++++++--- internal/handler/defintion_ast_test.go | 7 ++ internal/lsp/ast.go | 18 +++++ internal/lsp/ast_variable.go | 47 ++++++++++++ internal/lsp/ast_variable_test.go | 76 +++++++++++++++++++ internal/tree-sitter/gotemplate/node-types.go | 10 +++ pkg/chartutil/values.go | 1 + 8 files changed, 206 insertions(+), 37 deletions(-) create mode 100644 internal/handler/defintion_ast_test.go create mode 100644 internal/lsp/ast_variable.go create mode 100644 internal/lsp/ast_variable_test.go create mode 100644 internal/tree-sitter/gotemplate/node-types.go diff --git a/internal/handler/completion.go b/internal/handler/completion.go index 3026ec0d..46ad90cc 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,22 +121,8 @@ 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 FindRelevantChildNode(currentNode *sitter.Node, pointToLoopUp sitter.Point) { + panic("unimplemented") } func (h *langHandler) getValue(values chartutil.Values, splittedVar []string) []lsp.CompletionItem { diff --git a/internal/handler/definition.go b/internal/handler/definition.go index 28e85ebb..af8c4bbb 100644 --- a/internal/handler/definition.go +++ b/internal/handler/definition.go @@ -8,7 +8,10 @@ import ( "path/filepath" "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" ) @@ -31,11 +34,11 @@ func (h *langHandler) handleDefinition(ctx context.Context, reply jsonrpc2.Repli } var ( - word = doc.ValueAt(params.Position) - splitted = strings.Split(word, ".") - variableSplitted = []string{} - position lsp.Position - defitionFilePath string + word = doc.ValueAt(params.Position) + splitted = strings.Split(word, ".") + variableSplitted = []string{} + position lsp.Position + definitionFilePath string ) if word == "" { @@ -58,20 +61,20 @@ func (h *langHandler) handleDefinition(ctx context.Context, reply jsonrpc2.Repli switch variableSplitted[0] { case "Values": - defitionFilePath = filepath.Join(h.rootURI.Filename(), "values.yaml") + definitionFilePath = filepath.Join(h.rootURI.Filename(), "values.yaml") if len(variableSplitted) > 1 { position, err = h.getValueDefinition(variableSplitted[1:]) } case "Chart": - defitionFilePath = filepath.Join(h.rootURI.Filename(), "Chart.yaml") + definitionFilePath = filepath.Join(h.rootURI.Filename(), "Chart.yaml") if len(variableSplitted) > 1 { position, err = h.getChartDefinition(variableSplitted[1:]) } } - if err == nil && defitionFilePath != "" { + if err == nil && definitionFilePath != "" { result := lsp.Location{ - URI: "file://" + lsp.DocumentURI(defitionFilePath), + URI: "file://" + lsp.DocumentURI(definitionFilePath), Range: lsp.Range{Start: position}, } @@ -81,6 +84,34 @@ func (h *langHandler) handleDefinition(ctx context.Context, reply jsonrpc2.Repli return reply(ctx, nil, err) } +func definitionAstParsing(doc *lsplocal.Document, position lsp.Position) string { + var ( + currentNode = lsplocal.NodeAtPosition(doc.Ast, position) + pointToLoopUp = sitter.Point{ + Row: position.Line, + Column: position.Character, + } + relevantChildNode = lsplocal.FindRelevantChildNode(currentNode, pointToLoopUp) + word string + ) + + switch relevantChildNode.Type() { + case gotemplate.NodeTypeIdentifier: + if relevantChildNode.Parent().Type() == gotemplate.NodeTypeVariable { + + variableName := relevantChildNode.Content([]byte(doc.Content)) + lsplocal.GetVariableDefinition(variableName, relevantChildNode.Parent(), doc) + } + case gotemplate.NodeTypeDot: + word = lsplocal.TraverseIdentifierPathUp(relevantChildNode, doc) + case gotemplate.NodeTypeDotSymbol: + word = lsplocal.GetFieldIdentifierPath(relevantChildNode, doc) + } + + return word + +} + func (h *langHandler) getValueDefinition(splittedVar []string) (lsp.Position, error) { return util.GetPositionOfNode(h.valueNode, splittedVar) } diff --git a/internal/handler/defintion_ast_test.go b/internal/handler/defintion_ast_test.go new file mode 100644 index 00000000..a31332fb --- /dev/null +++ b/internal/handler/defintion_ast_test.go @@ -0,0 +1,7 @@ +package handler + +import "testing" + +func TestDefinitionAstParsingWithVariable(t *testing.T) { + +} diff --git a/internal/lsp/ast.go b/internal/lsp/ast.go index 426b26d8..a9076023 100644 --- a/internal/lsp/ast.go +++ b/internal/lsp/ast.go @@ -20,6 +20,24 @@ 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) { + 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 GetFieldIdentifierPath(node *sitter.Node, doc *Document) (path string) { path = buildFieldIdentifierPath(node, doc) logger.Println("buildFieldIdentifierPath:", path) diff --git a/internal/lsp/ast_variable.go b/internal/lsp/ast_variable.go new file mode 100644 index 00000000..481da8f8 --- /dev/null +++ b/internal/lsp/ast_variable.go @@ -0,0 +1,47 @@ +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.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.Println("GetVariableDefinition:", node.Type()) + + switch node.Type() { + case gotemplate.NodeTypeRangeVariableDefinition: + indexDefinition := node.NamedChild(0).Child(1).Content([]byte(template)) + elementDefinition := node.NamedChild(1).Child(1).Content([]byte(template)) + if indexDefinition == variableName || + elementDefinition == variableName { + return node + } + case gotemplate.NodeTypeVariableDefinition: + currentVariableName := node.ChildByFieldName("variable").Child(1).Content([]byte(template)) + logger.Println("currentVariableName:", currentVariableName) + logger.Println("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..39f0163e --- /dev/null +++ b/internal/lsp/ast_variable_test.go @@ -0,0 +1,76 @@ +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") + } + + 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))) + } +} diff --git a/internal/tree-sitter/gotemplate/node-types.go b/internal/tree-sitter/gotemplate/node-types.go new file mode 100644 index 00000000..693b9513 --- /dev/null +++ b/internal/tree-sitter/gotemplate/node-types.go @@ -0,0 +1,10 @@ +package gotemplate + +const ( + NodeTypeIdentifier = "identifier" + NodeTypeVariable = "variable" + NodeTypeDot = "dot" + NodeTypeDotSymbol = "." + NodeTypeVariableDefinition = "variable_definition" + NodeTypeRangeVariableDefinition = "range_variable_definition" +) diff --git a/pkg/chartutil/values.go b/pkg/chartutil/values.go index 1adfde25..87037c68 100644 --- a/pkg/chartutil/values.go +++ b/pkg/chartutil/values.go @@ -19,6 +19,7 @@ package chartutil import ( "fmt" "io" + "io/ioutil" "os" "strings" From dedc4fdd1012ea87fc0d65689f7095de4f9655db Mon Sep 17 00:00:00 2001 From: qvalentin Date: Wed, 26 Jul 2023 21:46:03 +0200 Subject: [PATCH 3/8] feat(definition): WIP: refactor go to definition --- internal/handler/definition.go | 57 +++++++++++++++++++++++++++----- internal/util/yaml_path.go | 60 ++++++++++++++++++++++++++++++++++ 2 files changed, 109 insertions(+), 8 deletions(-) create mode 100644 internal/util/yaml_path.go diff --git a/internal/handler/definition.go b/internal/handler/definition.go index af8c4bbb..1f0d4943 100644 --- a/internal/handler/definition.go +++ b/internal/handler/definition.go @@ -84,7 +84,10 @@ func (h *langHandler) handleDefinition(ctx context.Context, reply jsonrpc2.Repli return reply(ctx, nil, err) } -func definitionAstParsing(doc *lsplocal.Document, position lsp.Position) string { +// definitionAstParsing takes the current node +// depending on the node type it either returns the node that defines the current variable +// or the yaml selector for the current value +func (h *langHandler) definitionAstParsing(doc *lsplocal.Document, position lsp.Position) lsp.Location { var ( currentNode = lsplocal.NodeAtPosition(doc.Ast, position) pointToLoopUp = sitter.Point{ @@ -92,7 +95,6 @@ func definitionAstParsing(doc *lsplocal.Document, position lsp.Position) string Column: position.Character, } relevantChildNode = lsplocal.FindRelevantChildNode(currentNode, pointToLoopUp) - word string ) switch relevantChildNode.Type() { @@ -100,16 +102,54 @@ func definitionAstParsing(doc *lsplocal.Document, position lsp.Position) string if relevantChildNode.Parent().Type() == gotemplate.NodeTypeVariable { variableName := relevantChildNode.Content([]byte(doc.Content)) - lsplocal.GetVariableDefinition(variableName, relevantChildNode.Parent(), doc) + var node = lsplocal.GetVariableDefinition(variableName, relevantChildNode.Parent(), doc.Content) + return lsp.Location{URI: doc.URI, Range: lsp.Range{Start: node.StartPoint(), End: node.EndPoint()}} } - case gotemplate.NodeTypeDot: - word = lsplocal.TraverseIdentifierPathUp(relevantChildNode, doc) - case gotemplate.NodeTypeDotSymbol: - word = lsplocal.GetFieldIdentifierPath(relevantChildNode, doc) + case gotemplate.NodeTypeDot, gotemplate.NodeTypeDotSymbol: + return h.getDefinitionForValue(relevantChildNode, doc) + } + + return lsp.Location{} +} + +func (h *langHandler) getDefinitionForValue(node *sitter.Node, doc *lsplocal.Document) lsp.Location { + var ( + yamlPathString = getYamlPath(node, doc) + yamlPath, err = util.NewYamlPath(yamlPathString) + definitionFilePath string + position lsp.Position + ) + if err != nil { + return lsp.Location{} + } + + if yamlPath.IsValuesPath() { + definitionFilePath = filepath.Join(h.rootURI.Filename(), "values.yaml") + position, err = h.getValueDefinition(yamlPath.GetTail()) + } + if yamlPath.IsChartPath() { + definitionFilePath = filepath.Join(h.rootURI.Filename(), "Chart.yaml") + position, err = h.getChartDefinition(yamlPath.GetTail()) } - return word + if err == nil && definitionFilePath != "" { + return lsp.Location{ + URI: "file://" + lsp.DocumentURI(definitionFilePath), + Range: lsp.Range{Start: position}, + } + } + return lsp.Location{} +} +func getYamlPath(node *sitter.Node, doc *lsplocal.Document) string { + switch node.Type() { + case gotemplate.NodeTypeDot: + return lsplocal.TraverseIdentifierPathUp(node, doc) + case gotemplate.NodeTypeDotSymbol: + return lsplocal.GetFieldIdentifierPath(node, doc) + default: + return "" + } } func (h *langHandler) getValueDefinition(splittedVar []string) (lsp.Position, error) { @@ -119,6 +159,7 @@ func (h *langHandler) getChartDefinition(splittedVar []string) (lsp.Position, er modifyedVar := make([]string, 0) + // for Releases, we make the first letter lowercase TODO: only do this when really needed for _, value := range splittedVar { restOfString := "" if (len(value)) > 1 { diff --git a/internal/util/yaml_path.go b/internal/util/yaml_path.go new file mode 100644 index 00000000..9e6fe6b6 --- /dev/null +++ b/internal/util/yaml_path.go @@ -0,0 +1,60 @@ +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) + } + } + // $ 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:] + } + + if len(variableSplitted) == 0 { + return YamlPath{}, fmt.Errorf("Could not parse yaml path: %s", yamlPathString) + } + + 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" +} From 7735bf006ba6baaa49f60de4cfa98cfca52e5214 Mon Sep 17 00:00:00 2001 From: qvalentin Date: Thu, 27 Jul 2023 12:16:17 +0200 Subject: [PATCH 4/8] feat(definition): definition for variables finished --- internal/handler/definition.go | 127 +++++++----------- internal/handler/handler.go | 17 +-- internal/handler/project_files.go | 39 ++++++ internal/tree-sitter/gotemplate/node-types.go | 1 + internal/util/points.go | 14 ++ internal/util/yaml.go | 12 +- internal/util/yaml_path.go | 7 +- internal/util/yaml_test.go | 6 +- pkg/chartutil/values.go | 1 - 9 files changed, 124 insertions(+), 100 deletions(-) create mode 100644 internal/handler/project_files.go create mode 100644 internal/util/points.go diff --git a/internal/handler/definition.go b/internal/handler/definition.go index 1f0d4943..9c8ecada 100644 --- a/internal/handler/definition.go +++ b/internal/handler/definition.go @@ -5,7 +5,7 @@ import ( "encoding/json" "errors" "fmt" - "path/filepath" + "log" "strings" lsplocal "github.com/mrjosh/helm-ls/internal/lsp" @@ -17,8 +17,6 @@ import ( ) func (h *langHandler) handleDefinition(ctx context.Context, reply jsonrpc2.Replier, req jsonrpc2.Request) (err error) { - - logger.Println(fmt.Sprintf("Definition provider")) if req.Params() == nil { return &jsonrpc2.Error{Code: jsonrpc2.InvalidParams} } @@ -32,62 +30,17 @@ func (h *langHandler) handleDefinition(ctx context.Context, reply jsonrpc2.Repli if !ok { return errors.New("Could not get document: " + params.TextDocument.URI.Filename()) } + result, err := h.definitionAstParsing(doc, params.Position) - var ( - word = doc.ValueAt(params.Position) - splitted = strings.Split(word, ".") - variableSplitted = []string{} - position lsp.Position - definitionFilePath string - ) - - if word == "" { - return reply(ctx, nil, err) - } - - for _, s := range splitted { - if s != "" { - variableSplitted = append(variableSplitted, s) - } - } - - // $ 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:] - } - - logger.Println(fmt.Sprintf("Definition checking for word < %s >", word)) - - switch variableSplitted[0] { - case "Values": - definitionFilePath = filepath.Join(h.rootURI.Filename(), "values.yaml") - if len(variableSplitted) > 1 { - position, err = h.getValueDefinition(variableSplitted[1:]) - } - case "Chart": - definitionFilePath = filepath.Join(h.rootURI.Filename(), "Chart.yaml") - if len(variableSplitted) > 1 { - position, err = h.getChartDefinition(variableSplitted[1:]) - } - } - - if err == nil && definitionFilePath != "" { - result := lsp.Location{ - URI: "file://" + lsp.DocumentURI(definitionFilePath), - Range: lsp.Range{Start: position}, - } + log.Println("result", result) - return reply(ctx, result, err) + if err != nil { + return reply(ctx, err, err) } - logger.Printf("Had no match for definition. Error: %v", err) - return reply(ctx, nil, err) + return reply(ctx, result, err) } -// definitionAstParsing takes the current node -// depending on the node type it either returns the node that defines the current variable -// or the yaml selector for the current value -func (h *langHandler) definitionAstParsing(doc *lsplocal.Document, position lsp.Position) lsp.Location { +func (h *langHandler) definitionAstParsing(doc *lsplocal.Document, position lsp.Position) (lsp.Location, error) { var ( currentNode = lsplocal.NodeAtPosition(doc.Ast, position) pointToLoopUp = sitter.Point{ @@ -100,52 +53,74 @@ func (h *langHandler) definitionAstParsing(doc *lsplocal.Document, position lsp. switch relevantChildNode.Type() { case gotemplate.NodeTypeIdentifier: if relevantChildNode.Parent().Type() == gotemplate.NodeTypeVariable { - - variableName := relevantChildNode.Content([]byte(doc.Content)) - var node = lsplocal.GetVariableDefinition(variableName, relevantChildNode.Parent(), doc.Content) - return lsp.Location{URI: doc.URI, Range: lsp.Range{Start: node.StartPoint(), End: node.EndPoint()}} + return h.getDefinitionForVariable(relevantChildNode, doc) } - case gotemplate.NodeTypeDot, gotemplate.NodeTypeDotSymbol: + return h.getDefinitionForFixedIdentifier(relevantChildNode, doc) + case gotemplate.NodeTypeDot, gotemplate.NodeTypeDotSymbol, gotemplate.NodeTypeFieldIdentifier: return h.getDefinitionForValue(relevantChildNode, doc) } - return lsp.Location{} + 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 } -func (h *langHandler) getDefinitionForValue(node *sitter.Node, doc *lsplocal.Document) lsp.Location { +// 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) - definitionFilePath string - position lsp.Position + yamlPathString = getYamlPath(node, doc) + yamlPath, err = util.NewYamlPath(yamlPathString) + definitionFileURI lsp.DocumentURI + position lsp.Position ) if err != nil { - return lsp.Location{} + return lsp.Location{}, err } if yamlPath.IsValuesPath() { - definitionFilePath = filepath.Join(h.rootURI.Filename(), "values.yaml") + definitionFileURI = h.projectFiles.GetValuesFileURI() position, err = h.getValueDefinition(yamlPath.GetTail()) } if yamlPath.IsChartPath() { - definitionFilePath = filepath.Join(h.rootURI.Filename(), "Chart.yaml") + definitionFileURI = h.projectFiles.GetChartFileURI() position, err = h.getChartDefinition(yamlPath.GetTail()) } - if err == nil && definitionFilePath != "" { + if err == nil && definitionFileURI != "" { return lsp.Location{ - URI: "file://" + lsp.DocumentURI(definitionFilePath), + URI: definitionFileURI, Range: lsp.Range{Start: position}, - } + }, nil } - return lsp.Location{} + 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: + case gotemplate.NodeTypeDotSymbol, gotemplate.NodeTypeFieldIdentifier: return lsplocal.GetFieldIdentifierPath(node, doc) default: return "" @@ -155,11 +130,10 @@ func getYamlPath(node *sitter.Node, doc *lsplocal.Document) string { func (h *langHandler) getValueDefinition(splittedVar []string) (lsp.Position, error) { return util.GetPositionOfNode(h.valueNode, splittedVar) } -func (h *langHandler) getChartDefinition(splittedVar []string) (lsp.Position, error) { +func (h *langHandler) getChartDefinition(splittedVar []string) (lsp.Position, error) { modifyedVar := make([]string, 0) - - // for Releases, we make the first letter lowercase TODO: only do this when really needed + // for Charts, we make the first letter lowercase for _, value := range splittedVar { restOfString := "" if (len(value)) > 1 { @@ -168,6 +142,5 @@ func (h *langHandler) getChartDefinition(splittedVar []string) (lsp.Position, er firstLetterLowercase := strings.ToLower(string(value[0])) + restOfString modifyedVar = append(modifyedVar, firstLetterLowercase) } - return util.GetPositionOfNode(h.chartNode, modifyedVar) } diff --git a/internal/handler/handler.go b/internal/handler/handler.go index de1deec1..9ec11760 100644 --- a/internal/handler/handler.go +++ b/internal/handler/handler.go @@ -25,23 +25,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 - rootURI uri.URI } 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{}), - valueNode: yamlv3.Node{}, - chartNode: yamlv3.Node{}, - rootURI: "", + 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) @@ -93,6 +93,7 @@ func (h *langHandler) handleInitialize(ctx context.Context, reply jsonrpc2.Repli } vf := filepath.Join(workspace_uri.Path, "values.yaml") + h.projectFiles = NewProjectFiles(uri.URI(params.WorkspaceFolders[0].URI), "") vals, err := chartutil.ReadValuesFile(vf) if err != 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/tree-sitter/gotemplate/node-types.go b/internal/tree-sitter/gotemplate/node-types.go index 693b9513..4241a608 100644 --- a/internal/tree-sitter/gotemplate/node-types.go +++ b/internal/tree-sitter/gotemplate/node-types.go @@ -7,4 +7,5 @@ const ( 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 index 5d7d201f..bc6fc7f8 100644 --- a/internal/util/yaml.go +++ b/internal/util/yaml.go @@ -10,7 +10,7 @@ import ( 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) + return lsp.Position{}, fmt.Errorf("could not find Position of %s in values.yaml. Node was zero", query) } println(node.Value) @@ -24,15 +24,13 @@ func GetPositionOfNode(node yamlv3.Node, query []string) (lsp.Position, error) { 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) - } else { - return GetPositionOfNode(*node.Content[index+1], query[1:]) + return lsp.Position{}, fmt.Errorf("could not find Position of %s in values.yaml", query) } - } else { - return lsp.Position{Line: uint32(value.Line) - 1, Character: uint32(value.Column) - 1}, nil + 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) + 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 index 9e6fe6b6..d07c4e84 100644 --- a/internal/util/yaml_path.go +++ b/internal/util/yaml_path.go @@ -21,16 +21,15 @@ func NewYamlPath(yamlPathString string) (YamlPath, error) { 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:] } - if len(variableSplitted) == 0 { - return YamlPath{}, fmt.Errorf("Could not parse yaml path: %s", yamlPathString) - } - return YamlPath{ TableNames: variableSplitted, }, nil diff --git a/internal/util/yaml_test.go b/internal/util/yaml_test.go index af48843a..01b3a867 100644 --- a/internal/util/yaml_test.go +++ b/internal/util/yaml_test.go @@ -26,7 +26,7 @@ func TestGetPositionOfNode(t *testing.T) { } result, err := GetPositionOfNode(node, []string{"replicaCount"}) - expected := lsp.Position{Line: 6, Character: 1} + expected := lsp.Position{Line: 5, Character: 0} if err != nil { t.Errorf("Result had error: %s", err) } @@ -35,7 +35,7 @@ func TestGetPositionOfNode(t *testing.T) { } result, err = GetPositionOfNode(node, []string{"image", "repository"}) - expected = lsp.Position{Line: 9, Character: 3} + expected = lsp.Position{Line: 8, Character: 2} if err != nil { t.Errorf("Result had error: %s", err) } @@ -44,7 +44,7 @@ func TestGetPositionOfNode(t *testing.T) { } result, err = GetPositionOfNode(node, []string{"service", "test", "nested", "value"}) - expected = lsp.Position{Line: 31, Character: 7} + expected = lsp.Position{Line: 30, Character: 6} if err != nil { t.Errorf("Result had error: %s", err) } diff --git a/pkg/chartutil/values.go b/pkg/chartutil/values.go index 87037c68..1adfde25 100644 --- a/pkg/chartutil/values.go +++ b/pkg/chartutil/values.go @@ -19,7 +19,6 @@ package chartutil import ( "fmt" "io" - "io/ioutil" "os" "strings" From 0c8cc6fd75679bee4adce4741e9a15faaa782c8e Mon Sep 17 00:00:00 2001 From: qvalentin Date: Fri, 28 Jul 2023 16:33:54 +0200 Subject: [PATCH 5/8] [FIX] tests for ast_variable --- internal/lsp/ast_variable.go | 21 +++++++++++++++------ internal/lsp/ast_variable_test.go | 18 +++++++++++++++--- 2 files changed, 30 insertions(+), 9 deletions(-) diff --git a/internal/lsp/ast_variable.go b/internal/lsp/ast_variable.go index 481da8f8..b8245507 100644 --- a/internal/lsp/ast_variable.go +++ b/internal/lsp/ast_variable.go @@ -6,6 +6,9 @@ import ( ) func GetVariableDefinitionOfNode(node *sitter.Node, template string) *sitter.Node { + if node == nil { + return nil + } if node.Type() != gotemplate.NodeTypeVariable { return nil } @@ -21,15 +24,21 @@ func GetVariableDefinition(variableName string, node *sitter.Node, template stri return nil } - logger.Println("GetVariableDefinition:", node.Type()) + logger.Println("GetVariableDefinition:", node.Type(), variableName) switch node.Type() { case gotemplate.NodeTypeRangeVariableDefinition: - indexDefinition := node.NamedChild(0).Child(1).Content([]byte(template)) - elementDefinition := node.NamedChild(1).Child(1).Content([]byte(template)) - if indexDefinition == variableName || - elementDefinition == variableName { - return node + 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)) diff --git a/internal/lsp/ast_variable_test.go b/internal/lsp/ast_variable_test.go index 39f0163e..5a085811 100644 --- a/internal/lsp/ast_variable_test.go +++ b/internal/lsp/ast_variable_test.go @@ -54,6 +54,7 @@ func TestGetVariableDefinitionOtherDecleration(t *testing.T) { } } + func TestGetVariableDefinitionRange(t *testing.T) { var template = ` {{ range $index, $element := pipeline }}{{ $index }}{{ $element }}{{ end }} @@ -65,12 +66,23 @@ func TestGetVariableDefinitionRange(t *testing.T) { t.Errorf("Parsing did not work") } - usageNode := node.NamedChild(5) - definitionNode := GetVariableDefinitionOfNode(usageNode, template) + 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)) != "$variable := \"text\"" { + } 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))) } } From 2d4c67f611d11c698b1ef50e832ac9aa2aed69e6 Mon Sep 17 00:00:00 2001 From: qvalentin Date: Fri, 28 Jul 2023 16:34:22 +0200 Subject: [PATCH 6/8] [FIX] use projectFiles in handler --- internal/handler/handler.go | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/internal/handler/handler.go b/internal/handler/handler.go index 9ec11760..00a5b5d3 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" @@ -87,35 +85,33 @@ 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") - h.projectFiles = NewProjectFiles(uri.URI(params.WorkspaceFolders[0].URI), "") + h.projectFiles = NewProjectFiles(workspaceURI, "") - vals, err := chartutil.ReadValuesFile(vf) + 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.ReadYamlFileToNodes(vf) + valueNodes, err := chartutil.ReadYamlFileToNodes(h.projectFiles.ValuesFile) if err != nil { return err } h.valueNode = valueNodes - chartNode, err := chartutil.ReadYamlFileToNodes(chartFile) + chartNode, err := chartutil.ReadYamlFileToNodes(h.projectFiles.ChartFile) if err != nil { return err } From b7db0ba05f807511e36502f2516a26ef364eece3 Mon Sep 17 00:00:00 2001 From: qvalentin Date: Fri, 28 Jul 2023 19:38:22 +0200 Subject: [PATCH 7/8] [TEST] integration tests for go to definition --- internal/handler/definition_test.go | 157 +++++++++++++++++++++++++ internal/handler/defintion_ast_test.go | 7 -- 2 files changed, 157 insertions(+), 7 deletions(-) create mode 100644 internal/handler/definition_test.go delete mode 100644 internal/handler/defintion_ast_test.go diff --git a/internal/handler/definition_test.go b/internal/handler/definition_test.go new file mode 100644 index 00000000..c088b7e8 --- /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 +// -----| +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/defintion_ast_test.go b/internal/handler/defintion_ast_test.go deleted file mode 100644 index a31332fb..00000000 --- a/internal/handler/defintion_ast_test.go +++ /dev/null @@ -1,7 +0,0 @@ -package handler - -import "testing" - -func TestDefinitionAstParsingWithVariable(t *testing.T) { - -} From 8fedf31fc23e4db9c33cfd5cbe3ef993e0f78367 Mon Sep 17 00:00:00 2001 From: qvalentin Date: Thu, 21 Sep 2023 20:30:16 +0200 Subject: [PATCH 8/8] [FIX] cleanup code --- internal/handler/completion.go | 4 ---- internal/handler/definition.go | 9 +++++---- internal/handler/definition_test.go | 2 +- internal/handler/handler.go | 4 ++-- internal/lsp/ast.go | 8 +++----- internal/lsp/ast_variable.go | 8 +++----- internal/tree-sitter/gotemplate/node-types.go | 1 + internal/util/yaml.go | 2 -- pkg/chartutil/values.go | 4 ++-- 9 files changed, 17 insertions(+), 25 deletions(-) diff --git a/internal/handler/completion.go b/internal/handler/completion.go index 46ad90cc..191feee7 100644 --- a/internal/handler/completion.go +++ b/internal/handler/completion.go @@ -121,10 +121,6 @@ func completionAstParsing(doc *lsplocal.Document, position lsp.Position) string return word } -func FindRelevantChildNode(currentNode *sitter.Node, pointToLoopUp sitter.Point) { - panic("unimplemented") -} - 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 9c8ecada..34c855ea 100644 --- a/internal/handler/definition.go +++ b/internal/handler/definition.go @@ -5,7 +5,6 @@ import ( "encoding/json" "errors" "fmt" - "log" "strings" lsplocal "github.com/mrjosh/helm-ls/internal/lsp" @@ -32,10 +31,12 @@ func (h *langHandler) handleDefinition(ctx context.Context, reply jsonrpc2.Repli } result, err := h.definitionAstParsing(doc, params.Position) - log.Println("result", result) - if err != nil { - return reply(ctx, err, err) + // 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) } diff --git a/internal/handler/definition_test.go b/internal/handler/definition_test.go index c088b7e8..9b9ab57b 100644 --- a/internal/handler/definition_test.go +++ b/internal/handler/definition_test.go @@ -74,7 +74,7 @@ func genericDefinitionTest(t *testing.T, position lsp.Position, expectedLocation // 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, diff --git a/internal/handler/handler.go b/internal/handler/handler.go index 00a5b5d3..a74dc289 100644 --- a/internal/handler/handler.go +++ b/internal/handler/handler.go @@ -105,13 +105,13 @@ func (h *langHandler) handleInitialize(ctx context.Context, reply jsonrpc2.Repli return err } h.chartMetadata = *chartMetadata - valueNodes, err := chartutil.ReadYamlFileToNodes(h.projectFiles.ValuesFile) + valueNodes, err := chartutil.ReadYamlFileToNode(h.projectFiles.ValuesFile) if err != nil { return err } h.valueNode = valueNodes - chartNode, err := chartutil.ReadYamlFileToNodes(h.projectFiles.ChartFile) + chartNode, err := chartutil.ReadYamlFileToNode(h.projectFiles.ChartFile) if err != nil { return err } diff --git a/internal/lsp/ast.go b/internal/lsp/ast.go index a9076023..2de46ebf 100644 --- a/internal/lsp/ast.go +++ b/internal/lsp/ast.go @@ -24,7 +24,6 @@ func FindRelevantChildNode(currentNode *sitter.Node, pointToLookUp sitter.Point) 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) } } @@ -40,12 +39,11 @@ func isPointLargerOrEq(a sitter.Point, b sitter.Point) bool { 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)) @@ -80,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 index b8245507..696c9540 100644 --- a/internal/lsp/ast_variable.go +++ b/internal/lsp/ast_variable.go @@ -12,11 +12,9 @@ func GetVariableDefinitionOfNode(node *sitter.Node, template string) *sitter.Nod 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 { @@ -24,7 +22,7 @@ func GetVariableDefinition(variableName string, node *sitter.Node, template stri return nil } - logger.Println("GetVariableDefinition:", node.Type(), variableName) + logger.Debug("GetVariableDefinition:", node.Type(), variableName) switch node.Type() { case gotemplate.NodeTypeRangeVariableDefinition: @@ -42,8 +40,8 @@ func GetVariableDefinition(variableName string, node *sitter.Node, template stri } case gotemplate.NodeTypeVariableDefinition: currentVariableName := node.ChildByFieldName("variable").Child(1).Content([]byte(template)) - logger.Println("currentVariableName:", currentVariableName) - logger.Println("variableName:", variableName) + logger.Debug("currentVariableName:", currentVariableName) + logger.Debug("variableName:", variableName) if currentVariableName == variableName { return node } diff --git a/internal/tree-sitter/gotemplate/node-types.go b/internal/tree-sitter/gotemplate/node-types.go index 4241a608..25bdcf91 100644 --- a/internal/tree-sitter/gotemplate/node-types.go +++ b/internal/tree-sitter/gotemplate/node-types.go @@ -4,6 +4,7 @@ const ( NodeTypeIdentifier = "identifier" NodeTypeVariable = "variable" NodeTypeDot = "dot" + NodeTypeDollar = "$" NodeTypeDotSymbol = "." NodeTypeVariableDefinition = "variable_definition" NodeTypeRangeVariableDefinition = "range_variable_definition" diff --git a/internal/util/yaml.go b/internal/util/yaml.go index bc6fc7f8..140b6ecd 100644 --- a/internal/util/yaml.go +++ b/internal/util/yaml.go @@ -8,11 +8,9 @@ import ( ) 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) } - println(node.Value) for index, value := range node.Content { if value.Value == "" { diff --git a/pkg/chartutil/values.go b/pkg/chartutil/values.go index 1adfde25..192339b5 100644 --- a/pkg/chartutil/values.go +++ b/pkg/chartutil/values.go @@ -153,8 +153,8 @@ func ReadValuesFile(filename string) (Values, error) { return ReadValues(data) } -// ReadYamlFileToNodes will parse a YAML file into a yaml Nodes. -func ReadYamlFileToNodes(filename string) (node yamlv3.Node, err error) { +// 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