Skip to content

Commit

Permalink
feat(definition): go to defintion for values and Chart
Browse files Browse the repository at this point in the history
  • Loading branch information
qvalentin committed Apr 10, 2023
1 parent ed7a270 commit d1f7f46
Show file tree
Hide file tree
Showing 8 changed files with 272 additions and 2 deletions.
82 changes: 80 additions & 2 deletions internal/handler/definition.go
Original file line number Diff line number Diff line change
Expand Up @@ -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}
}
Expand All @@ -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)
}
24 changes: 24 additions & 0 deletions internal/handler/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand All @@ -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 {
Expand All @@ -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)
Expand Down Expand Up @@ -69,13 +77,28 @@ 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 {
return err
}
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{
Expand All @@ -89,6 +112,7 @@ func (h *langHandler) handleInitialize(ctx context.Context, reply jsonrpc2.Repli
TriggerCharacters: []string{".", "$."},
ResolveProvider: false,
},
DefinitionProvider: true,
},
}, nil)
}
Expand Down
11 changes: 11 additions & 0 deletions internal/lsp/document.go
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
20 changes: 20 additions & 0 deletions internal/util/strings.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 ""
}
38 changes: 38 additions & 0 deletions internal/util/yaml.go
Original file line number Diff line number Diff line change
@@ -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)

}
54 changes: 54 additions & 0 deletions internal/util/yaml_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
33 changes: 33 additions & 0 deletions internal/util/yaml_test_input.yaml
Original file line number Diff line number Diff line change
@@ -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


12 changes: 12 additions & 0 deletions pkg/chartutil/values.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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 {
Expand Down

0 comments on commit d1f7f46

Please sign in to comment.