Skip to content

Commit

Permalink
Full completion support (#93)
Browse files Browse the repository at this point in the history
Closes #5
Simplified the code in the process.
Completion was using it's own stack-walking code, I moved it to use the same thing as go-to-definition
  • Loading branch information
julienduchesne authored May 2, 2023
1 parent a7565a7 commit 4214e85
Show file tree
Hide file tree
Showing 7 changed files with 177 additions and 87 deletions.
39 changes: 22 additions & 17 deletions pkg/ast/processing/find_field.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import (
log "github.com/sirupsen/logrus"
)

func FindRangesFromIndexList(stack *nodestack.NodeStack, indexList []string, vm *jsonnet.VM) ([]ObjectRange, error) {
func FindRangesFromIndexList(stack *nodestack.NodeStack, indexList []string, vm *jsonnet.VM, partialMatchFields bool) ([]ObjectRange, error) {
var foundDesugaredObjects []*ast.DesugaredObject
// First element will be super, self, or var name
start, indexList := indexList[0], indexList[1:]
Expand Down Expand Up @@ -45,7 +45,7 @@ func FindRangesFromIndexList(stack *nodestack.NodeStack, indexList []string, vm
// Get ast.DesugaredObject at variable definition by getting bind then setting ast.DesugaredObject
bind := FindBindByIDViaStack(stack, ast.Identifier(start))
if bind == nil {
param := FindParameterByIDViaStack(stack, ast.Identifier(start))
param := FindParameterByIDViaStack(stack, ast.Identifier(start), partialMatchFields)
if param != nil {
return []ObjectRange{
{
Expand All @@ -69,7 +69,7 @@ func FindRangesFromIndexList(stack *nodestack.NodeStack, indexList []string, vm
case *ast.Index, *ast.Apply:
tempStack := nodestack.NewNodeStack(bodyNode)
indexList = append(tempStack.BuildIndexList(), indexList...)
return FindRangesFromIndexList(stack, indexList, vm)
return FindRangesFromIndexList(stack, indexList, vm, partialMatchFields)
case *ast.Function:
// If the function's body is an object, it means we can look for indexes within the function
if funcBody := findChildDesugaredObject(bodyNode.Body); funcBody != nil {
Expand All @@ -80,15 +80,15 @@ func FindRangesFromIndexList(stack *nodestack.NodeStack, indexList []string, vm
}
}

return extractObjectRangesFromDesugaredObjs(stack, vm, foundDesugaredObjects, sameFileOnly, indexList)
return extractObjectRangesFromDesugaredObjs(stack, vm, foundDesugaredObjects, sameFileOnly, indexList, partialMatchFields)
}

func extractObjectRangesFromDesugaredObjs(stack *nodestack.NodeStack, vm *jsonnet.VM, desugaredObjs []*ast.DesugaredObject, sameFileOnly bool, indexList []string) ([]ObjectRange, error) {
func extractObjectRangesFromDesugaredObjs(stack *nodestack.NodeStack, vm *jsonnet.VM, desugaredObjs []*ast.DesugaredObject, sameFileOnly bool, indexList []string, partialMatchFields bool) ([]ObjectRange, error) {
var ranges []ObjectRange
for len(indexList) > 0 {
index := indexList[0]
indexList = indexList[1:]
foundFields := findObjectFieldsInObjects(desugaredObjs, index)
foundFields := findObjectFieldsInObjects(desugaredObjs, index, partialMatchFields)
desugaredObjs = nil
if len(foundFields) == 0 {
return nil, fmt.Errorf("field %s was not found in ast.DesugaredObject", index)
Expand All @@ -98,7 +98,8 @@ func extractObjectRangesFromDesugaredObjs(stack *nodestack.NodeStack, vm *jsonne
ranges = append(ranges, FieldToRange(*found))

// If the field is not PlusSuper (field+: value), we stop there. Other previous values are not relevant
if !found.PlusSuper {
// If partialMatchFields is true, we can continue to look for other fields
if !found.PlusSuper && !partialMatchFields {
break
}
}
Expand Down Expand Up @@ -134,7 +135,7 @@ func extractObjectRangesFromDesugaredObjs(stack *nodestack.NodeStack, vm *jsonne
desugaredObjs = append(desugaredObjs, fieldNode)
case *ast.Index:
additionalIndexList := append(nodestack.NewNodeStack(fieldNode).BuildIndexList(), indexList...)
result, err := FindRangesFromIndexList(stack, additionalIndexList, vm)
result, err := FindRangesFromIndexList(stack, additionalIndexList, vm, partialMatchFields)
if len(result) > 0 {
if !sameFileOnly || result[0].Filename == stack.From.Loc().FileName {
return result, err
Expand Down Expand Up @@ -186,32 +187,36 @@ func unpackFieldNodes(vm *jsonnet.VM, fields []*ast.DesugaredObjectField) ([]ast
return fieldNodes, nil
}

func findObjectFieldsInObjects(objectNodes []*ast.DesugaredObject, index string) []*ast.DesugaredObjectField {
func findObjectFieldsInObjects(objectNodes []*ast.DesugaredObject, index string, partialMatchFields bool) []*ast.DesugaredObjectField {
var matchingFields []*ast.DesugaredObjectField
for _, object := range objectNodes {
field := findObjectFieldInObject(object, index)
if field != nil {
matchingFields = append(matchingFields, field)
}
fields := findObjectFieldsInObject(object, index, partialMatchFields)
matchingFields = append(matchingFields, fields...)
}
return matchingFields
}

func findObjectFieldInObject(objectNode *ast.DesugaredObject, index string) *ast.DesugaredObjectField {
func findObjectFieldsInObject(objectNode *ast.DesugaredObject, index string, partialMatchFields bool) []*ast.DesugaredObjectField {
if objectNode == nil {
return nil
}

var matchingFields []*ast.DesugaredObjectField
for _, field := range objectNode.Fields {
field := field
literalString, isString := field.Name.(*ast.LiteralString)
if !isString {
continue
}
log.Debugf("Checking index name %s against field name %s", index, literalString.Value)
if index == literalString.Value {
return &field
if index == literalString.Value || (partialMatchFields && strings.HasPrefix(literalString.Value, index)) {
matchingFields = append(matchingFields, &field)
if !partialMatchFields {
break
}
}
}
return nil
return matchingFields
}

func findChildDesugaredObject(node ast.Node) *ast.DesugaredObject {
Expand Down
6 changes: 4 additions & 2 deletions pkg/ast/processing/find_param.go
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
package processing

import (
"strings"

"github.com/google/go-jsonnet/ast"
"github.com/grafana/jsonnet-language-server/pkg/nodestack"
)

func FindParameterByIDViaStack(stack *nodestack.NodeStack, id ast.Identifier) *ast.Parameter {
func FindParameterByIDViaStack(stack *nodestack.NodeStack, id ast.Identifier, partialMatchFields bool) *ast.Parameter {
for _, node := range stack.Stack {
if f, ok := node.(*ast.Function); ok {
for _, param := range f.Parameters {
if param.Name == id {
if param.Name == id || (partialMatchFields && strings.HasPrefix(string(param.Name), string(id))) {
return &param
}
}
Expand Down
4 changes: 4 additions & 0 deletions pkg/ast/processing/object_range.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ type ObjectRange struct {
Filename string
SelectionRange ast.LocationRange
FullRange ast.LocationRange
FieldName string
Node ast.Node
}

func FieldToRange(field ast.DesugaredObjectField) ObjectRange {
Expand All @@ -28,6 +30,8 @@ func FieldToRange(field ast.DesugaredObjectField) ObjectRange {
Filename: field.LocRange.FileName,
SelectionRange: selectionRange,
FullRange: field.LocRange,
FieldName: FieldNameToString(field.Name),
Node: field.Body,
}
}

Expand Down
6 changes: 3 additions & 3 deletions pkg/ast/processing/top_level_objects.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,9 @@ func FindTopLevelObjects(stack *nodestack.NodeStack, vm *jsonnet.VM) []*ast.Desu
if !indexIsString {
continue
}
obj := findObjectFieldInObject(containerObj, indexValue.Value)
if obj != nil {
stack.Push(obj.Body)
objs := findObjectFieldsInObject(containerObj, indexValue.Value, false)
if len(objs) > 0 {
stack.Push(objs[0].Body)
}
}
case *ast.Var:
Expand Down
111 changes: 48 additions & 63 deletions pkg/server/completion.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ package server

import (
"context"
"reflect"
"sort"
"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 @@ -63,17 +63,16 @@ func (s *Server) completionFromStack(line string, stack *nodestack.NodeStack, vm
lastWord = strings.TrimRight(lastWord, ",;") // Ignore trailing commas and semicolons, they can present when someone is modifying an existing line

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

if len(indexes) == 0 {
if len(indexes) == 1 {
var items []protocol.CompletionItem
// firstIndex is a variable (local) completion
for !stack.IsEmpty() {
if curr, ok := stack.Pop().(*ast.Local); ok {
for _, bind := range curr.Binds {
label := string(bind.Variable)

if !strings.HasPrefix(label, firstIndex) {
if !strings.HasPrefix(label, indexes[0]) {
continue
}

Expand All @@ -84,46 +83,14 @@ func (s *Server) completionFromStack(line string, stack *nodestack.NodeStack, vm
return items
}

if len(indexes) > 1 {
// TODO: Support multiple indexes, the objects to search through will be the reference in the last index
ranges, err := processing.FindRangesFromIndexList(stack, indexes, vm, true)
if err != nil {
log.Errorf("Completion: error finding ranges: %v", err)
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.Self: // This case catches `$` references (it's set as a self reference on the root object)
objectsToSearch = processing.FindTopLevelObjects(nodestack.NewNodeStack(stack.From), vm)
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)
completionPrefix := strings.Join(indexes[:len(indexes)-1], ".")
return createCompletionItemsFromRanges(ranges, completionPrefix, line)
}

func (s *Server) completionStdLib(line string) []protocol.CompletionItem {
Expand Down Expand Up @@ -165,31 +132,24 @@ func (s *Server) completionStdLib(line string) []protocol.CompletionItem {
return items
}

func createCompletionItemsFromObjects(objects []*ast.DesugaredObject, firstIndex, fieldPrefix, currentLine string) []protocol.CompletionItem {
func createCompletionItemsFromRanges(ranges []processing.ObjectRange, completionPrefix, 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
}
for _, field := range ranges {
label := field.FieldName

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

items = append(items, createCompletionItem(label, firstIndex+"."+label, protocol.FieldCompletion, field.Body))
labels[label] = true
// Ignore the current field
if strings.Contains(currentLine, label+":") && completionPrefix == "self" {
continue
}

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

sort.Slice(items, func(i, j int) bool {
Expand All @@ -213,9 +173,34 @@ func createCompletionItem(label, detail string, kind protocol.CompletionItemKind
}

return protocol.CompletionItem{
Label: label,
Detail: detail,
Kind: kind,
Label: label,
Detail: detail,
Kind: kind,
LabelDetails: protocol.CompletionItemLabelDetails{
Description: typeToString(body),
},
InsertText: insertText,
}
}

func typeToString(t ast.Node) string {
switch t.(type) {
case *ast.Array:
return "array"
case *ast.LiteralBoolean:
return "boolean"
case *ast.Function:
return "function"
case *ast.LiteralNull:
return "null"
case *ast.LiteralNumber:
return "number"
case *ast.Object, *ast.DesugaredObject:
return "object"
case *ast.LiteralString:
return "string"
case *ast.Import, *ast.ImportStr:
return "import"
}
return reflect.TypeOf(t).String()
}
Loading

0 comments on commit 4214e85

Please sign in to comment.