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

Full completion support #93

Merged
merged 1 commit into from
May 2, 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
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