From 4214e85a4a08360a5ba328aaddc9e4bf0fcafbad Mon Sep 17 00:00:00 2001 From: Julien Duchesne Date: Tue, 2 May 2023 07:34:56 -0400 Subject: [PATCH] Full completion support (#93) Closes https://github.com/grafana/jsonnet-language-server/issues/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 --- pkg/ast/processing/find_field.go | 39 +++++---- pkg/ast/processing/find_param.go | 6 +- pkg/ast/processing/object_range.go | 4 + pkg/ast/processing/top_level_objects.go | 6 +- pkg/server/completion.go | 111 ++++++++++-------------- pkg/server/completion_test.go | 94 ++++++++++++++++++++ pkg/server/definition.go | 4 +- 7 files changed, 177 insertions(+), 87 deletions(-) diff --git a/pkg/ast/processing/find_field.go b/pkg/ast/processing/find_field.go index c34704c..a5765aa 100644 --- a/pkg/ast/processing/find_field.go +++ b/pkg/ast/processing/find_field.go @@ -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:] @@ -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{ { @@ -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 { @@ -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) @@ -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 } } @@ -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 @@ -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 { diff --git a/pkg/ast/processing/find_param.go b/pkg/ast/processing/find_param.go index 146f9c1..33a170f 100644 --- a/pkg/ast/processing/find_param.go +++ b/pkg/ast/processing/find_param.go @@ -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 ¶m } } diff --git a/pkg/ast/processing/object_range.go b/pkg/ast/processing/object_range.go index 6d09720..1d5be49 100644 --- a/pkg/ast/processing/object_range.go +++ b/pkg/ast/processing/object_range.go @@ -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 { @@ -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, } } diff --git a/pkg/ast/processing/top_level_objects.go b/pkg/ast/processing/top_level_objects.go index d0a2ad8..b628808 100644 --- a/pkg/ast/processing/top_level_objects.go +++ b/pkg/ast/processing/top_level_objects.go @@ -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: diff --git a/pkg/server/completion.go b/pkg/server/completion.go index 67a3637..9bf413c 100644 --- a/pkg/server/completion.go +++ b/pkg/server/completion.go @@ -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" @@ -63,9 +63,8 @@ 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() { @@ -73,7 +72,7 @@ func (s *Server) completionFromStack(line string, stack *nodestack.NodeStack, vm for _, bind := range curr.Binds { label := string(bind.Variable) - if !strings.HasPrefix(label, firstIndex) { + if !strings.HasPrefix(label, indexes[0]) { continue } @@ -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 { @@ -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 { @@ -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() +} diff --git a/pkg/server/completion_test.go b/pkg/server/completion_test.go index 22ea761..f991da7 100644 --- a/pkg/server/completion_test.go +++ b/pkg/server/completion_test.go @@ -162,6 +162,9 @@ func TestCompletion(t *testing.T) { Kind: protocol.FunctionCompletion, Detail: "self.greet(name)", InsertText: "greet(name)", + LabelDetails: protocol.CompletionItemLabelDetails{ + Description: "function", + }, }}, }, }, @@ -187,6 +190,9 @@ func TestCompletion(t *testing.T) { Kind: protocol.FunctionCompletion, Detail: "self.greet(name)", InsertText: "greet(name)", + LabelDetails: protocol.CompletionItemLabelDetails{ + Description: "function", + }, }}, }, }, @@ -202,6 +208,9 @@ func TestCompletion(t *testing.T) { Kind: protocol.FieldCompletion, Detail: "self.foo", InsertText: "foo", + LabelDetails: protocol.CompletionItemLabelDetails{ + Description: "string", + }, }}, }, }, @@ -217,6 +226,9 @@ func TestCompletion(t *testing.T) { Kind: protocol.VariableCompletion, Detail: "somevar", InsertText: "somevar", + LabelDetails: protocol.CompletionItemLabelDetails{ + Description: "string", + }, }}, }, }, @@ -232,6 +244,9 @@ func TestCompletion(t *testing.T) { Kind: protocol.VariableCompletion, Detail: "somevar", InsertText: "somevar", + LabelDetails: protocol.CompletionItemLabelDetails{ + Description: "string", + }, }}, }, }, @@ -258,12 +273,18 @@ func TestCompletion(t *testing.T) { Kind: protocol.FieldCompletion, Detail: "otherfile.bar", InsertText: "bar", + LabelDetails: protocol.CompletionItemLabelDetails{ + Description: "string", + }, }, { Label: "foo", Kind: protocol.FieldCompletion, Detail: "otherfile.foo", InsertText: "foo", + LabelDetails: protocol.CompletionItemLabelDetails{ + Description: "string", + }, }, }, }, @@ -281,6 +302,9 @@ func TestCompletion(t *testing.T) { Kind: protocol.FieldCompletion, Detail: "otherfile.bar", InsertText: "bar", + LabelDetails: protocol.CompletionItemLabelDetails{ + Description: "string", + }, }, }, }, @@ -298,12 +322,18 @@ func TestCompletion(t *testing.T) { Kind: protocol.FieldCompletion, Detail: "$.attribute", InsertText: "attribute", + LabelDetails: protocol.CompletionItemLabelDetails{ + Description: "object", + }, }, { Label: "attribute2", Kind: protocol.FieldCompletion, Detail: "$.attribute2", InsertText: "attribute2", + LabelDetails: protocol.CompletionItemLabelDetails{ + Description: "object", + }, }, }, }, @@ -321,12 +351,76 @@ func TestCompletion(t *testing.T) { Kind: protocol.FieldCompletion, Detail: "$.attribute", InsertText: "attribute", + LabelDetails: protocol.CompletionItemLabelDetails{ + Description: "object", + }, }, { Label: "attribute2", Kind: protocol.FieldCompletion, Detail: "$.attribute2", InsertText: "attribute2", + LabelDetails: protocol.CompletionItemLabelDetails{ + Description: "object", + }, + }, + }, + }, + }, + { + name: "autocomplete nested imported file", + filename: "testdata/goto-nested-imported-file.jsonnet", + replaceString: "foo: file.foo,", + replaceByString: "foo: file.", + expected: protocol.CompletionList{ + IsIncomplete: false, + Items: []protocol.CompletionItem{ + { + Label: "bar", + Kind: protocol.FieldCompletion, + Detail: "file.bar", + InsertText: "bar", + LabelDetails: protocol.CompletionItemLabelDetails{ + Description: "string", + }, + }, + { + Label: "foo", + Kind: protocol.FieldCompletion, + Detail: "file.foo", + InsertText: "foo", + LabelDetails: protocol.CompletionItemLabelDetails{ + Description: "string", + }, + }, + }, + }, + }, + { + name: "autocomplete multiple fields within local", + filename: "testdata/goto-indexes.jsonnet", + replaceString: "attr: obj.foo", + replaceByString: "attr: obj.", + expected: protocol.CompletionList{ + IsIncomplete: false, + Items: []protocol.CompletionItem{ + { + Label: "bar", + Kind: protocol.FieldCompletion, + Detail: "obj.bar", + InsertText: "bar", + LabelDetails: protocol.CompletionItemLabelDetails{ + Description: "string", + }, + }, + { + Label: "foo", + Kind: protocol.FieldCompletion, + Detail: "obj.foo", + InsertText: "foo", + LabelDetails: protocol.CompletionItemLabelDetails{ + Description: "object", + }, }, }, }, diff --git a/pkg/server/definition.go b/pkg/server/definition.go index ecbe272..071d585 100644 --- a/pkg/server/definition.go +++ b/pkg/server/definition.go @@ -74,7 +74,7 @@ func findDefinition(root ast.Node, params *protocol.DefinitionParams, vm *jsonne if bind := processing.FindBindByIDViaStack(searchStack, deepestNode.Id); bind != nil { objectRange = processing.LocalBindToRange(*bind) - } else if param := processing.FindParameterByIDViaStack(searchStack, deepestNode.Id); param != nil { + } else if param := processing.FindParameterByIDViaStack(searchStack, deepestNode.Id, false); param != nil { objectRange = processing.ObjectRange{ Filename: param.LocRange.FileName, FullRange: param.LocRange, @@ -93,7 +93,7 @@ func findDefinition(root ast.Node, params *protocol.DefinitionParams, vm *jsonne indexSearchStack := nodestack.NewNodeStack(deepestNode) indexList := indexSearchStack.BuildIndexList() tempSearchStack := *searchStack - objectRanges, err := processing.FindRangesFromIndexList(&tempSearchStack, indexList, vm) + objectRanges, err := processing.FindRangesFromIndexList(&tempSearchStack, indexList, vm, false) if err != nil { return nil, err }