Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat(definition): go to defintion for values and Chart #24

Merged
merged 8 commits into from
Sep 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 5 additions & 30 deletions internal/handler/completion.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,14 @@
"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"
lsp "go.lsp.dev/protocol"
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)
Expand Down Expand Up @@ -81,9 +74,9 @@
}

switch variableSplitted[0] {
case "Chart":

Check failure on line 77 in internal/handler/completion.go

View workflow job for this annotation

GitHub Actions / lint (1.19.1, ubuntu-latest)

string `Chart` has 3 occurrences, make it a constant (goconst)
items = getVariableCompletionItems(chartVals)
case "Values":

Check failure on line 79 in internal/handler/completion.go

View workflow job for this annotation

GitHub Actions / lint (1.19.1, ubuntu-latest)

string `Values` has 3 occurrences, make it a constant (goconst)
items = h.getValue(h.values, variableSplitted[1:])
case "Release":
items = getVariableCompletionItems(releaseVals)
Expand All @@ -107,45 +100,27 @@
Row: position.Line,
Column: position.Character,
}
relevantChildNode = findRelevantChildNode(currentNode, pointToLoopUp)
relevantChildNode = lsplocal.FindRelevantChildNode(currentNode, pointToLoopUp)
word string
)

logger.Println("currentNode", currentNode)
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)
}

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 (
Expand Down
134 changes: 131 additions & 3 deletions internal/handler/definition.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,145 @@
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(), &params)
if err := json.Unmarshal(req.Params(), &params); 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

Check failure on line 35 in internal/handler/definition.go

View workflow job for this annotation

GitHub Actions / lint (1.19.1, ubuntu-latest)

`supress` is a misspelling of `suppress` (misspell)
// 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)
}
157 changes: 157 additions & 0 deletions internal/handler/definition_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
Loading
Loading