Skip to content

Commit

Permalink
Merge pull request #24 from qvalentin/feat-values-definition
Browse files Browse the repository at this point in the history
feat(definition): go to defintion for values and Chart
  • Loading branch information
qvalentin authored Sep 30, 2023
2 parents a6fc99e + 8fedf31 commit e534e0e
Show file tree
Hide file tree
Showing 15 changed files with 742 additions and 49 deletions.
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 @@ 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"
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 @@ -107,45 +100,27 @@ 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
)

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 @@ package handler
import (
"context"
"encoding/json"
"errors"
"fmt"
"strings"

lsplocal "github.com/mrjosh/helm-ls/internal/lsp"
gotemplate "github.com/mrjosh/helm-ls/internal/tree-sitter/gotemplate"
"github.com/mrjosh/helm-ls/internal/util"
sitter "github.com/smacker/go-tree-sitter"
"go.lsp.dev/jsonrpc2"
lsp "go.lsp.dev/protocol"
)

func (h *langHandler) handleDefinition(_ context.Context, _ jsonrpc2.Replier, req jsonrpc2.Request) (err error) {

func (h *langHandler) handleDefinition(ctx context.Context, reply jsonrpc2.Replier, req jsonrpc2.Request) (err error) {
if req.Params() == nil {
return &jsonrpc2.Error{Code: jsonrpc2.InvalidParams}
}

var params lsp.DefinitionParams
return json.Unmarshal(req.Params(), &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

0 comments on commit e534e0e

Please sign in to comment.