Skip to content

Commit 81ab873

Browse files
Completion from imports: Basic cases (#78)
* 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
1 parent 008c7bf commit 81ab873

File tree

4 files changed

+133
-48
lines changed

4 files changed

+133
-48
lines changed

pkg/ast/processing/find_field.go

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -32,14 +32,14 @@ func FindRangesFromIndexList(stack *nodestack.NodeStack, indexList []string, vm
3232
if _, ok := tmpStack.Peek().(*ast.Binary); ok {
3333
tmpStack.Pop()
3434
}
35-
foundDesugaredObjects = filterSelfScope(findTopLevelObjects(tmpStack, vm))
35+
foundDesugaredObjects = filterSelfScope(FindTopLevelObjects(tmpStack, vm))
3636
case start == "std":
3737
return nil, fmt.Errorf("cannot get definition of std lib")
3838
case start == "$":
3939
sameFileOnly = true
40-
foundDesugaredObjects = findTopLevelObjects(nodestack.NewNodeStack(stack.From), vm)
40+
foundDesugaredObjects = FindTopLevelObjects(nodestack.NewNodeStack(stack.From), vm)
4141
case strings.Contains(start, "."):
42-
foundDesugaredObjects = findTopLevelObjectsInFile(vm, start, "")
42+
foundDesugaredObjects = FindTopLevelObjectsInFile(vm, start, "")
4343

4444
default:
4545
// Get ast.DesugaredObject at variable definition by getting bind then setting ast.DesugaredObject
@@ -62,10 +62,10 @@ func FindRangesFromIndexList(stack *nodestack.NodeStack, indexList []string, vm
6262
foundDesugaredObjects = append(foundDesugaredObjects, bodyNode)
6363
case *ast.Self:
6464
tmpStack := nodestack.NewNodeStack(stack.From)
65-
foundDesugaredObjects = findTopLevelObjects(tmpStack, vm)
65+
foundDesugaredObjects = FindTopLevelObjects(tmpStack, vm)
6666
case *ast.Import:
6767
filename := bodyNode.File.Value
68-
foundDesugaredObjects = findTopLevelObjectsInFile(vm, filename, "")
68+
foundDesugaredObjects = FindTopLevelObjectsInFile(vm, filename, "")
6969
case *ast.Index, *ast.Apply:
7070
tempStack := nodestack.NewNodeStack(bodyNode)
7171
indexList = append(tempStack.BuildIndexList(), indexList...)
@@ -116,10 +116,10 @@ func extractObjectRangesFromDesugaredObjs(stack *nodestack.NodeStack, vm *jsonne
116116
switch fieldNode := fieldNode.(type) {
117117
case *ast.Apply:
118118
// Add the target of the Apply to the list of field nodes to look for
119-
// The target is a function and will be found by findVarReference on the next loop
119+
// The target is a function and will be found by FindVarReference on the next loop
120120
fieldNodes = append(fieldNodes, fieldNode.Target)
121121
case *ast.Var:
122-
varReference, err := findVarReference(fieldNode, vm)
122+
varReference, err := FindVarReference(fieldNode, vm)
123123
if err != nil {
124124
return nil, err
125125
}
@@ -149,7 +149,7 @@ func extractObjectRangesFromDesugaredObjs(stack *nodestack.NodeStack, vm *jsonne
149149
desugaredObjs = append(desugaredObjs, findChildDesugaredObject(fieldNode.Body))
150150
case *ast.Import:
151151
filename := fieldNode.File.Value
152-
newObjs := findTopLevelObjectsInFile(vm, filename, string(fieldNode.Loc().File.DiagnosticFileName))
152+
newObjs := FindTopLevelObjectsInFile(vm, filename, string(fieldNode.Loc().File.DiagnosticFileName))
153153
desugaredObjs = append(desugaredObjs, newObjs...)
154154
}
155155
i++
@@ -232,9 +232,9 @@ func findChildDesugaredObject(node ast.Node) *ast.DesugaredObject {
232232
return nil
233233
}
234234

235-
// findVarReference finds the object that the variable is referencing
235+
// FindVarReference finds the object that the variable is referencing
236236
// To do so, we get the stack where the var is used and search that stack for the var's definition
237-
func findVarReference(varNode *ast.Var, vm *jsonnet.VM) (ast.Node, error) {
237+
func FindVarReference(varNode *ast.Var, vm *jsonnet.VM) (ast.Node, error) {
238238
varFileNode, _, _ := vm.ImportAST("", varNode.LocRange.FileName)
239239
varStack, err := FindNodeByPosition(varFileNode, varNode.Loc().Begin)
240240
if err != nil {

pkg/ast/processing/top_level_objects.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,18 @@ import (
99

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

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

1919
return fileTopLevelObjectsCache[cacheKey]
2020
}
2121

2222
// Find all ast.DesugaredObject's from NodeStack
23-
func findTopLevelObjects(stack *nodestack.NodeStack, vm *jsonnet.VM) []*ast.DesugaredObject {
23+
func FindTopLevelObjects(stack *nodestack.NodeStack, vm *jsonnet.VM) []*ast.DesugaredObject {
2424
var objects []*ast.DesugaredObject
2525
for !stack.IsEmpty() {
2626
curr := stack.Pop()
@@ -49,7 +49,7 @@ func findTopLevelObjects(stack *nodestack.NodeStack, vm *jsonnet.VM) []*ast.Desu
4949
}
5050
}
5151
case *ast.Var:
52-
varReference, err := findVarReference(curr, vm)
52+
varReference, err := FindVarReference(curr, vm)
5353
if err != nil {
5454
log.WithError(err).Errorf("Error finding var reference, ignoring this node")
5555
continue

pkg/server/completion.go

Lines changed: 78 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ import (
44
"context"
55
"strings"
66

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

41-
items := s.completionFromStack(line, searchStack)
43+
vm := s.getVM(doc.item.URI.SpanURI().Filename())
44+
45+
items := s.completionFromStack(line, searchStack, vm)
4246
return &protocol.CompletionList{IsIncomplete: false, Items: items}, nil
4347
}
4448

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

55-
func (s *Server) completionFromStack(line string, stack *nodestack.NodeStack) []protocol.CompletionItem {
56-
var items []protocol.CompletionItem
57-
59+
func (s *Server) completionFromStack(line string, stack *nodestack.NodeStack, vm *jsonnet.VM) []protocol.CompletionItem {
5860
lineWords := strings.Split(line, " ")
5961
lastWord := lineWords[len(lineWords)-1]
6062

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

64-
if firstIndex == "self" && len(indexes) > 0 {
65-
fieldPrefix := indexes[0]
66-
67-
for !stack.IsEmpty() {
68-
curr := stack.Pop()
69-
70-
switch curr := curr.(type) {
71-
case *ast.Binary:
72-
stack.Push(curr.Left)
73-
stack.Push(curr.Right)
74-
case *ast.DesugaredObject:
75-
for _, field := range curr.Fields {
76-
label := processing.FieldNameToString(field.Name)
77-
// Ignore fields that don't match the prefix
78-
if !strings.HasPrefix(label, fieldPrefix) {
79-
continue
80-
}
81-
82-
// Ignore the current field
83-
if strings.Contains(line, label+":") {
84-
continue
85-
}
86-
87-
items = append(items, createCompletionItem(label, "self."+label, protocol.FieldCompletion, field.Body))
88-
}
89-
}
90-
}
91-
} else if len(indexes) == 0 {
66+
if len(indexes) == 0 {
67+
var items []protocol.CompletionItem
9268
// firstIndex is a variable (local) completion
9369
for !stack.IsEmpty() {
9470
if curr, ok := stack.Pop().(*ast.Local); ok {
@@ -103,13 +79,51 @@ func (s *Server) completionFromStack(line string, stack *nodestack.NodeStack) []
10379
}
10480
}
10581
}
82+
return items
10683
}
10784

108-
return items
85+
if len(indexes) > 1 {
86+
// TODO: Support multiple indexes, the objects to search through will be the reference in the last index
87+
return nil
88+
}
89+
90+
var (
91+
objectsToSearch []*ast.DesugaredObject
92+
)
93+
94+
if firstIndex == "self" {
95+
// Search through the current stack
96+
objectsToSearch = processing.FindTopLevelObjects(stack, vm)
97+
} else {
98+
// If the index is something other than 'self', find what it refers to (Var reference) and find objects in that
99+
for !stack.IsEmpty() {
100+
curr := stack.Pop()
101+
102+
if targetVar, ok := curr.(*ast.Var); ok && string(targetVar.Id) == firstIndex {
103+
ref, _ := processing.FindVarReference(targetVar, vm)
104+
105+
switch ref := ref.(type) {
106+
case *ast.DesugaredObject:
107+
objectsToSearch = []*ast.DesugaredObject{ref}
108+
case *ast.Import:
109+
filename := ref.File.Value
110+
objectsToSearch = processing.FindTopLevelObjectsInFile(vm, filename, string(curr.Loc().File.DiagnosticFileName))
111+
}
112+
break
113+
}
114+
115+
for _, node := range toolutils.Children(curr) {
116+
stack.Push(node)
117+
}
118+
}
119+
}
120+
121+
fieldPrefix := indexes[0]
122+
return createCompletionItemsFromObjects(objectsToSearch, firstIndex, fieldPrefix, line)
109123
}
110124

111125
func (s *Server) completionStdLib(line string) []protocol.CompletionItem {
112-
items := []protocol.CompletionItem{}
126+
var items []protocol.CompletionItem
113127

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

164+
func createCompletionItemsFromObjects(objects []*ast.DesugaredObject, firstIndex, fieldPrefix, currentLine string) []protocol.CompletionItem {
165+
var items []protocol.CompletionItem
166+
labels := make(map[string]bool)
167+
168+
for _, obj := range objects {
169+
for _, field := range obj.Fields {
170+
label := processing.FieldNameToString(field.Name)
171+
172+
if labels[label] {
173+
continue
174+
}
175+
176+
// Ignore fields that don't match the prefix
177+
if !strings.HasPrefix(label, fieldPrefix) {
178+
continue
179+
}
180+
181+
// Ignore the current field
182+
if strings.Contains(currentLine, label+":") {
183+
continue
184+
}
185+
186+
items = append(items, createCompletionItem(label, firstIndex+"."+label, protocol.FieldCompletion, field.Body))
187+
labels[label] = true
188+
}
189+
}
190+
191+
return items
192+
}
193+
150194
func createCompletionItem(label, detail string, kind protocol.CompletionItemKind, body ast.Node) protocol.CompletionItem {
151195
insertText := label
152196
if asFunc, ok := body.(*ast.Function); ok {

pkg/server/completion_test.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,13 +245,54 @@ func TestCompletion(t *testing.T) {
245245
Items: nil,
246246
},
247247
},
248+
{
249+
name: "autocomplete through import",
250+
filename: "testdata/goto-imported-file.jsonnet",
251+
replaceString: "b: otherfile.bar,",
252+
replaceByString: "b: otherfile.",
253+
expected: protocol.CompletionList{
254+
IsIncomplete: false,
255+
Items: []protocol.CompletionItem{
256+
{
257+
Label: "bar",
258+
Kind: protocol.FieldCompletion,
259+
Detail: "otherfile.bar",
260+
InsertText: "bar",
261+
},
262+
{
263+
Label: "foo",
264+
Kind: protocol.FieldCompletion,
265+
Detail: "otherfile.foo",
266+
InsertText: "foo",
267+
},
268+
},
269+
},
270+
},
271+
{
272+
name: "autocomplete through import with prefix",
273+
filename: "testdata/goto-imported-file.jsonnet",
274+
replaceString: "b: otherfile.bar,",
275+
replaceByString: "b: otherfile.b",
276+
expected: protocol.CompletionList{
277+
IsIncomplete: false,
278+
Items: []protocol.CompletionItem{
279+
{
280+
Label: "bar",
281+
Kind: protocol.FieldCompletion,
282+
Detail: "otherfile.bar",
283+
InsertText: "bar",
284+
},
285+
},
286+
},
287+
},
248288
}
249289
for _, tc := range testCases {
250290
t.Run(tc.name, func(t *testing.T) {
251291
content, err := os.ReadFile(tc.filename)
252292
require.NoError(t, err)
253293

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

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

0 commit comments

Comments
 (0)