diff --git a/internal/handler/definition.go b/internal/handler/definition.go index 97d617f5..28e85ebb 100644 --- a/internal/handler/definition.go +++ b/internal/handler/definition.go @@ -3,13 +3,19 @@ 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, reply 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} } @@ -19,5 +25,77 @@ func (h *langHandler) handleDefinition(_ context.Context, reply jsonrpc2.Replier return err } - return nil + 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 f7ca92fe..b39e428f 100644 --- a/internal/handler/handler.go +++ b/internal/handler/handler.go @@ -10,6 +10,8 @@ import ( "github.com/mrjosh/helm-ls/pkg/chartutil" "go.lsp.dev/jsonrpc2" lsp "go.lsp.dev/protocol" + "go.lsp.dev/uri" + "gopkg.in/yaml.v3" "github.com/mrjosh/helm-ls/internal/log" ) @@ -21,6 +23,9 @@ type langHandler struct { linterName string documents *lsplocal.DocumentStore values chartutil.Values + valueNode yaml.Node + chartNode yaml.Node + rootURI uri.URI } func NewHandler(connPool jsonrpc2.Conn) jsonrpc2.Handler { @@ -30,6 +35,9 @@ func NewHandler(connPool jsonrpc2.Conn) jsonrpc2.Handler { connPool: connPool, documents: lsplocal.NewDocumentStore(fileStorage), values: make(map[string]interface{}), + valueNode: yaml.Node{}, + chartNode: yaml.Node{}, + rootURI: "", } logger.Printf("helm-lint-langserver: connections opened") return jsonrpc2.ReplyHandler(handler.handle) @@ -69,6 +77,8 @@ func (h *langHandler) handleInitialize(ctx context.Context, reply jsonrpc2.Repli return err } + h.rootURI = params.RootURI + vf := filepath.Join(params.RootURI.Filename(), "values.yaml") vals, err := chartutil.ReadValuesFile(vf) if err != nil { @@ -76,6 +86,19 @@ func (h *langHandler) handleInitialize(ctx context.Context, reply jsonrpc2.Repli } h.values = vals + valueNodes, err := chartutil.ReadYamlFileToNodes(vf) + if err != nil { + return err + } + h.valueNode = valueNodes + + chartFile := filepath.Join(params.RootURI.Filename(), "Chart.yaml") + chartNode, err := chartutil.ReadYamlFileToNodes(chartFile) + if err != nil { + return err + } + h.chartNode = chartNode + return reply(ctx, lsp.InitializeResult{ Capabilities: lsp.ServerCapabilities{ TextDocumentSync: lsp.TextDocumentSyncOptions{ @@ -89,6 +112,7 @@ func (h *langHandler) handleInitialize(ctx context.Context, reply jsonrpc2.Repli TriggerCharacters: []string{".", "$."}, ResolveProvider: false, }, + DefinitionProvider: true, }, }, nil) } diff --git a/internal/lsp/document.go b/internal/lsp/document.go index 5fea63cb..f3e93004 100644 --- a/internal/lsp/document.go +++ b/internal/lsp/document.go @@ -95,6 +95,17 @@ func (d *document) WordAt(pos lsp.Position) string { return util.WordAt(line, int(pos.Character)) } +func (d *document) ValueAt(pos lsp.Position) string { + + logger.Debug(pos) + + line, ok := d.GetLine(int(pos.Line)) + if !ok { + return "" + } + return util.ValueAt(line, int(pos.Character)) +} + // ContentAtRange returns the document text at given range. func (d *document) ContentAtRange(rng lsp.Range) string { return d.Content[rng.Start.Character:rng.End.Character] diff --git a/internal/util/strings.go b/internal/util/strings.go index 26b2a3a7..eb9a1dbe 100644 --- a/internal/util/strings.go +++ b/internal/util/strings.go @@ -74,3 +74,23 @@ func WordAt(str string, index int) string { return "" } + +// ValueAt returns the value found at the given character position. +// It removes all content of the word after a "." right of the position. +func ValueAt(str string, index int) string { + + wordIdxs := wordRegex.FindAllStringIndex(str, -1) + for _, wordIdx := range wordIdxs { + if wordIdx[0] <= index && index <= wordIdx[1] { + leftOfWord := str[wordIdx[0] : index+1] + rightOfWord := str[index+1 : wordIdx[1]] + rightOfWordEnd := strings.Index(rightOfWord, ".") + if rightOfWordEnd == -1 { + rightOfWordEnd = len(rightOfWord) - 1 + } + return leftOfWord + rightOfWord[0:rightOfWordEnd+1] + } + } + + return "" +} diff --git a/internal/util/yaml.go b/internal/util/yaml.go new file mode 100644 index 00000000..7a6cd261 --- /dev/null +++ b/internal/util/yaml.go @@ -0,0 +1,38 @@ +package util + +import ( + "fmt" + + lsp "go.lsp.dev/protocol" + "gopkg.in/yaml.v3" +) + +func GetPositionOfNode(node yaml.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 29047b53..932705e4 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" @@ -121,6 +122,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 := ioutil.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 {