Skip to content

Commit

Permalink
Completion from imports: Basic cases (#78)
Browse files Browse the repository at this point in the history
* Completion from imports: Basic cases
Progress towards #5
Logic is starting to leak out of the processing lib as I haven't found an interface that makes sense for this
Some of this should probably be re-architected. However, I believe it is good enough for now and tests are in-place if we want to refactor a bit

* Better structure, fix lint

* Add unsupported case
  • Loading branch information
julienduchesne authored Oct 20, 2022
1 parent 008c7bf commit 81ab873
Show file tree
Hide file tree
Showing 4 changed files with 133 additions and 48 deletions.
20 changes: 10 additions & 10 deletions pkg/ast/processing/find_field.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,14 @@ func FindRangesFromIndexList(stack *nodestack.NodeStack, indexList []string, vm
if _, ok := tmpStack.Peek().(*ast.Binary); ok {
tmpStack.Pop()
}
foundDesugaredObjects = filterSelfScope(findTopLevelObjects(tmpStack, vm))
foundDesugaredObjects = filterSelfScope(FindTopLevelObjects(tmpStack, vm))
case start == "std":
return nil, fmt.Errorf("cannot get definition of std lib")
case start == "$":
sameFileOnly = true
foundDesugaredObjects = findTopLevelObjects(nodestack.NewNodeStack(stack.From), vm)
foundDesugaredObjects = FindTopLevelObjects(nodestack.NewNodeStack(stack.From), vm)
case strings.Contains(start, "."):
foundDesugaredObjects = findTopLevelObjectsInFile(vm, start, "")
foundDesugaredObjects = FindTopLevelObjectsInFile(vm, start, "")

default:
// Get ast.DesugaredObject at variable definition by getting bind then setting ast.DesugaredObject
Expand All @@ -62,10 +62,10 @@ func FindRangesFromIndexList(stack *nodestack.NodeStack, indexList []string, vm
foundDesugaredObjects = append(foundDesugaredObjects, bodyNode)
case *ast.Self:
tmpStack := nodestack.NewNodeStack(stack.From)
foundDesugaredObjects = findTopLevelObjects(tmpStack, vm)
foundDesugaredObjects = FindTopLevelObjects(tmpStack, vm)
case *ast.Import:
filename := bodyNode.File.Value
foundDesugaredObjects = findTopLevelObjectsInFile(vm, filename, "")
foundDesugaredObjects = FindTopLevelObjectsInFile(vm, filename, "")
case *ast.Index, *ast.Apply:
tempStack := nodestack.NewNodeStack(bodyNode)
indexList = append(tempStack.BuildIndexList(), indexList...)
Expand Down Expand Up @@ -116,10 +116,10 @@ func extractObjectRangesFromDesugaredObjs(stack *nodestack.NodeStack, vm *jsonne
switch fieldNode := fieldNode.(type) {
case *ast.Apply:
// Add the target of the Apply to the list of field nodes to look for
// The target is a function and will be found by findVarReference on the next loop
// The target is a function and will be found by FindVarReference on the next loop
fieldNodes = append(fieldNodes, fieldNode.Target)
case *ast.Var:
varReference, err := findVarReference(fieldNode, vm)
varReference, err := FindVarReference(fieldNode, vm)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -149,7 +149,7 @@ func extractObjectRangesFromDesugaredObjs(stack *nodestack.NodeStack, vm *jsonne
desugaredObjs = append(desugaredObjs, findChildDesugaredObject(fieldNode.Body))
case *ast.Import:
filename := fieldNode.File.Value
newObjs := findTopLevelObjectsInFile(vm, filename, string(fieldNode.Loc().File.DiagnosticFileName))
newObjs := FindTopLevelObjectsInFile(vm, filename, string(fieldNode.Loc().File.DiagnosticFileName))
desugaredObjs = append(desugaredObjs, newObjs...)
}
i++
Expand Down Expand Up @@ -232,9 +232,9 @@ func findChildDesugaredObject(node ast.Node) *ast.DesugaredObject {
return nil
}

// findVarReference finds the object that the variable is referencing
// FindVarReference finds the object that the variable is referencing
// To do so, we get the stack where the var is used and search that stack for the var's definition
func findVarReference(varNode *ast.Var, vm *jsonnet.VM) (ast.Node, error) {
func FindVarReference(varNode *ast.Var, vm *jsonnet.VM) (ast.Node, error) {
varFileNode, _, _ := vm.ImportAST("", varNode.LocRange.FileName)
varStack, err := FindNodeByPosition(varFileNode, varNode.Loc().Begin)
if err != nil {
Expand Down
8 changes: 4 additions & 4 deletions pkg/ast/processing/top_level_objects.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,18 @@ import (

var fileTopLevelObjectsCache = make(map[string][]*ast.DesugaredObject)

func findTopLevelObjectsInFile(vm *jsonnet.VM, filename, importedFrom string) []*ast.DesugaredObject {
func FindTopLevelObjectsInFile(vm *jsonnet.VM, filename, importedFrom string) []*ast.DesugaredObject {
cacheKey := importedFrom + ":" + filename
if _, ok := fileTopLevelObjectsCache[cacheKey]; !ok {
rootNode, _, _ := vm.ImportAST(importedFrom, filename)
fileTopLevelObjectsCache[cacheKey] = findTopLevelObjects(nodestack.NewNodeStack(rootNode), vm)
fileTopLevelObjectsCache[cacheKey] = FindTopLevelObjects(nodestack.NewNodeStack(rootNode), vm)
}

return fileTopLevelObjectsCache[cacheKey]
}

// Find all ast.DesugaredObject's from NodeStack
func findTopLevelObjects(stack *nodestack.NodeStack, vm *jsonnet.VM) []*ast.DesugaredObject {
func FindTopLevelObjects(stack *nodestack.NodeStack, vm *jsonnet.VM) []*ast.DesugaredObject {
var objects []*ast.DesugaredObject
for !stack.IsEmpty() {
curr := stack.Pop()
Expand Down Expand Up @@ -49,7 +49,7 @@ func findTopLevelObjects(stack *nodestack.NodeStack, vm *jsonnet.VM) []*ast.Desu
}
}
case *ast.Var:
varReference, err := findVarReference(curr, vm)
varReference, err := FindVarReference(curr, vm)
if err != nil {
log.WithError(err).Errorf("Error finding var reference, ignoring this node")
continue
Expand Down
112 changes: 78 additions & 34 deletions pkg/server/completion.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import (
"context"
"strings"

"github.com/google/go-jsonnet"
"github.com/google/go-jsonnet/ast"
"github.com/google/go-jsonnet/toolutils"
"github.com/grafana/jsonnet-language-server/pkg/ast/processing"
"github.com/grafana/jsonnet-language-server/pkg/nodestack"
position "github.com/grafana/jsonnet-language-server/pkg/position_conversion"
Expand Down Expand Up @@ -38,7 +40,9 @@ func (s *Server) Completion(ctx context.Context, params *protocol.CompletionPara
return nil, nil
}

items := s.completionFromStack(line, searchStack)
vm := s.getVM(doc.item.URI.SpanURI().Filename())

items := s.completionFromStack(line, searchStack, vm)
return &protocol.CompletionList{IsIncomplete: false, Items: items}, nil
}

Expand All @@ -52,43 +56,15 @@ func getCompletionLine(fileContent string, position protocol.Position) string {
return line
}

func (s *Server) completionFromStack(line string, stack *nodestack.NodeStack) []protocol.CompletionItem {
var items []protocol.CompletionItem

func (s *Server) completionFromStack(line string, stack *nodestack.NodeStack, vm *jsonnet.VM) []protocol.CompletionItem {
lineWords := strings.Split(line, " ")
lastWord := lineWords[len(lineWords)-1]

indexes := strings.Split(lastWord, ".")
firstIndex, indexes := indexes[0], indexes[1:]

if firstIndex == "self" && len(indexes) > 0 {
fieldPrefix := indexes[0]

for !stack.IsEmpty() {
curr := stack.Pop()

switch curr := curr.(type) {
case *ast.Binary:
stack.Push(curr.Left)
stack.Push(curr.Right)
case *ast.DesugaredObject:
for _, field := range curr.Fields {
label := processing.FieldNameToString(field.Name)
// Ignore fields that don't match the prefix
if !strings.HasPrefix(label, fieldPrefix) {
continue
}

// Ignore the current field
if strings.Contains(line, label+":") {
continue
}

items = append(items, createCompletionItem(label, "self."+label, protocol.FieldCompletion, field.Body))
}
}
}
} else if len(indexes) == 0 {
if len(indexes) == 0 {
var items []protocol.CompletionItem
// firstIndex is a variable (local) completion
for !stack.IsEmpty() {
if curr, ok := stack.Pop().(*ast.Local); ok {
Expand All @@ -103,13 +79,51 @@ func (s *Server) completionFromStack(line string, stack *nodestack.NodeStack) []
}
}
}
return items
}

return items
if len(indexes) > 1 {
// TODO: Support multiple indexes, the objects to search through will be the reference in the last index
return nil
}

var (
objectsToSearch []*ast.DesugaredObject
)

if firstIndex == "self" {
// Search through the current stack
objectsToSearch = processing.FindTopLevelObjects(stack, vm)
} else {
// If the index is something other than 'self', find what it refers to (Var reference) and find objects in that
for !stack.IsEmpty() {
curr := stack.Pop()

if targetVar, ok := curr.(*ast.Var); ok && string(targetVar.Id) == firstIndex {
ref, _ := processing.FindVarReference(targetVar, vm)

switch ref := ref.(type) {
case *ast.DesugaredObject:
objectsToSearch = []*ast.DesugaredObject{ref}
case *ast.Import:
filename := ref.File.Value
objectsToSearch = processing.FindTopLevelObjectsInFile(vm, filename, string(curr.Loc().File.DiagnosticFileName))
}
break
}

for _, node := range toolutils.Children(curr) {
stack.Push(node)
}
}
}

fieldPrefix := indexes[0]
return createCompletionItemsFromObjects(objectsToSearch, firstIndex, fieldPrefix, line)
}

func (s *Server) completionStdLib(line string) []protocol.CompletionItem {
items := []protocol.CompletionItem{}
var items []protocol.CompletionItem

stdIndex := strings.LastIndex(line, "std.")
if stdIndex != -1 {
Expand Down Expand Up @@ -147,6 +161,36 @@ func (s *Server) completionStdLib(line string) []protocol.CompletionItem {
return items
}

func createCompletionItemsFromObjects(objects []*ast.DesugaredObject, firstIndex, fieldPrefix, currentLine string) []protocol.CompletionItem {
var items []protocol.CompletionItem
labels := make(map[string]bool)

for _, obj := range objects {
for _, field := range obj.Fields {
label := processing.FieldNameToString(field.Name)

if labels[label] {
continue
}

// Ignore fields that don't match the prefix
if !strings.HasPrefix(label, fieldPrefix) {
continue
}

// Ignore the current field
if strings.Contains(currentLine, label+":") {
continue
}

items = append(items, createCompletionItem(label, firstIndex+"."+label, protocol.FieldCompletion, field.Body))
labels[label] = true
}
}

return items
}

func createCompletionItem(label, detail string, kind protocol.CompletionItemKind, body ast.Node) protocol.CompletionItem {
insertText := label
if asFunc, ok := body.(*ast.Function); ok {
Expand Down
41 changes: 41 additions & 0 deletions pkg/server/completion_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -245,13 +245,54 @@ func TestCompletion(t *testing.T) {
Items: nil,
},
},
{
name: "autocomplete through import",
filename: "testdata/goto-imported-file.jsonnet",
replaceString: "b: otherfile.bar,",
replaceByString: "b: otherfile.",
expected: protocol.CompletionList{
IsIncomplete: false,
Items: []protocol.CompletionItem{
{
Label: "bar",
Kind: protocol.FieldCompletion,
Detail: "otherfile.bar",
InsertText: "bar",
},
{
Label: "foo",
Kind: protocol.FieldCompletion,
Detail: "otherfile.foo",
InsertText: "foo",
},
},
},
},
{
name: "autocomplete through import with prefix",
filename: "testdata/goto-imported-file.jsonnet",
replaceString: "b: otherfile.bar,",
replaceByString: "b: otherfile.b",
expected: protocol.CompletionList{
IsIncomplete: false,
Items: []protocol.CompletionItem{
{
Label: "bar",
Kind: protocol.FieldCompletion,
Detail: "otherfile.bar",
InsertText: "bar",
},
},
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
content, err := os.ReadFile(tc.filename)
require.NoError(t, err)

server, fileURI := testServerWithFile(t, completionTestStdlib, string(content))
server.configuration.JPaths = []string{"testdata"}

replacedContent := strings.ReplaceAll(string(content), tc.replaceString, tc.replaceByString)

Expand Down

0 comments on commit 81ab873

Please sign in to comment.