From ddb587ac9fab515e84e0fd99e1850cca984059a8 Mon Sep 17 00:00:00 2001 From: Duologic Date: Thu, 7 Mar 2024 14:41:00 +0100 Subject: [PATCH 1/4] test(completion): quote labels --- pkg/server/completion_test.go | 77 +++++++++++++++++++++++++ pkg/server/testdata/quote_label.jsonnet | 7 +++ 2 files changed, 84 insertions(+) create mode 100644 pkg/server/testdata/quote_label.jsonnet diff --git a/pkg/server/completion_test.go b/pkg/server/completion_test.go index 92855d7..fa85eb9 100644 --- a/pkg/server/completion_test.go +++ b/pkg/server/completion_test.go @@ -586,6 +586,83 @@ func TestCompletion(t *testing.T) { }, }, }, + { + name: "quote label", + filename: "testdata/quote_label.jsonnet", + replaceString: "lib", + replaceByString: "lib.", + expected: protocol.CompletionList{ + IsIncomplete: false, + Items: []protocol.CompletionItem{ + { + Label: "1num", + Kind: protocol.FieldCompletion, + Detail: "lib['1num']", + InsertText: "['1num']", + LabelDetails: protocol.CompletionItemLabelDetails{ + Description: "string", + }, + TextEdit: &protocol.TextEdit{ + Range: protocol.Range{ + Start: protocol.Position{ + Line: 0, + Character: 9, + }, + End: protocol.Position{ + Line: 0, + Character: 10, + }, + }, + NewText: "['1num']", + }, + }, + { + Label: "abc#func", + Kind: protocol.FunctionCompletion, + Detail: "lib['abc#func'](param)", + InsertText: "['abc#func'](param)", + LabelDetails: protocol.CompletionItemLabelDetails{ + Description: "function", + }, + TextEdit: &protocol.TextEdit{ + Range: protocol.Range{ + Start: protocol.Position{ + Line: 0, + Character: 9, + }, + End: protocol.Position{ + Line: 0, + Character: 10, + }, + }, + NewText: "['abc#func'](param)", + }, + }, + { + Label: "abc#var", + Kind: protocol.FieldCompletion, + Detail: "lib['abc#var']", + InsertText: "['abc#var']", + LabelDetails: protocol.CompletionItemLabelDetails{ + Description: "string", + }, + TextEdit: &protocol.TextEdit{ + Range: protocol.Range{ + Start: protocol.Position{ + Line: 0, + Character: 9, + }, + End: protocol.Position{ + Line: 0, + Character: 10, + }, + }, + NewText: "['abc#var']", + }, + }, + }, + }, + }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { diff --git a/pkg/server/testdata/quote_label.jsonnet b/pkg/server/testdata/quote_label.jsonnet new file mode 100644 index 0000000..9cde847 --- /dev/null +++ b/pkg/server/testdata/quote_label.jsonnet @@ -0,0 +1,7 @@ +local lib = { + '1num': 'val', + 'abc#func'(param=1): param, + 'abc#var': 'val', +}; + +lib From 779ab8d99e37c3c5e7b21dfebbb4217e8e0f20f6 Mon Sep 17 00:00:00 2001 From: Duologic Date: Thu, 7 Mar 2024 15:00:02 +0100 Subject: [PATCH 2/4] feat(completion): quote labels when necessary --- pkg/server/completion.go | 64 +++++++++++++++++++++++++++++++++++----- 1 file changed, 56 insertions(+), 8 deletions(-) diff --git a/pkg/server/completion.go b/pkg/server/completion.go index 06c482c..ff7997c 100644 --- a/pkg/server/completion.go +++ b/pkg/server/completion.go @@ -43,7 +43,7 @@ func (s *Server) Completion(_ context.Context, params *protocol.CompletionParams vm := s.getVM(doc.item.URI.SpanURI().Filename()) - items := s.completionFromStack(line, searchStack, vm) + items := s.completionFromStack(line, searchStack, vm, params.Position) return &protocol.CompletionList{IsIncomplete: false, Items: items}, nil } @@ -57,7 +57,7 @@ func getCompletionLine(fileContent string, position protocol.Position) string { return line } -func (s *Server) completionFromStack(line string, stack *nodestack.NodeStack, vm *jsonnet.VM) []protocol.CompletionItem { +func (s *Server) completionFromStack(line string, stack *nodestack.NodeStack, vm *jsonnet.VM, position protocol.Position) []protocol.CompletionItem { lineWords := strings.Split(line, " ") lastWord := lineWords[len(lineWords)-1] lastWord = strings.TrimRight(lastWord, ",;") // Ignore trailing commas and semicolons, they can present when someone is modifying an existing line @@ -76,7 +76,7 @@ func (s *Server) completionFromStack(line string, stack *nodestack.NodeStack, vm continue } - items = append(items, createCompletionItem(label, label, protocol.VariableCompletion, bind.Body)) + items = append(items, createCompletionItem(label, "", protocol.VariableCompletion, bind.Body, position)) } } } @@ -90,7 +90,7 @@ func (s *Server) completionFromStack(line string, stack *nodestack.NodeStack, vm } completionPrefix := strings.Join(indexes[:len(indexes)-1], ".") - return createCompletionItemsFromRanges(ranges, completionPrefix, line) + return createCompletionItemsFromRanges(ranges, completionPrefix, line, position) } func (s *Server) completionStdLib(line string) []protocol.CompletionItem { @@ -132,7 +132,7 @@ func (s *Server) completionStdLib(line string) []protocol.CompletionItem { return items } -func createCompletionItemsFromRanges(ranges []processing.ObjectRange, completionPrefix, currentLine string) []protocol.CompletionItem { +func createCompletionItemsFromRanges(ranges []processing.ObjectRange, completionPrefix, currentLine string, position protocol.Position) []protocol.CompletionItem { var items []protocol.CompletionItem labels := make(map[string]bool) @@ -152,7 +152,7 @@ func createCompletionItemsFromRanges(ranges []processing.ObjectRange, completion continue } - items = append(items, createCompletionItem(label, completionPrefix+"."+label, protocol.FieldCompletion, field.Node)) + items = append(items, createCompletionItem(label, completionPrefix, protocol.FieldCompletion, field.Node, position)) labels[label] = true } @@ -163,8 +163,20 @@ func createCompletionItemsFromRanges(ranges []processing.ObjectRange, completion return items } -func createCompletionItem(label, detail string, kind protocol.CompletionItemKind, body ast.Node) protocol.CompletionItem { +func createCompletionItem(label, prefix string, kind protocol.CompletionItemKind, body ast.Node, position protocol.Position) protocol.CompletionItem { + runes := []rune(label) + mustQuoteLabel := !(hasOnlyLetter(runes[0:1]) && len(runes) > 1 && hasOnlyLettersAndNumber(runes[1:])) + insertText := label + detail := label + if prefix != "" { + detail = prefix + "." + insertText + } + if mustQuoteLabel { + insertText = "['" + label + "']" + detail = prefix + insertText + } + if asFunc, ok := body.(*ast.Function); ok { kind = protocol.FunctionCompletion params := []string{} @@ -176,7 +188,7 @@ func createCompletionItem(label, detail string, kind protocol.CompletionItemKind insertText += paramsString } - return protocol.CompletionItem{ + item := protocol.CompletionItem{ Label: label, Detail: detail, Kind: kind, @@ -185,6 +197,42 @@ func createCompletionItem(label, detail string, kind protocol.CompletionItemKind }, InsertText: insertText, } + + // Remove leading `.` character when quoting label + if mustQuoteLabel { + item.TextEdit = &protocol.TextEdit{ + Range: protocol.Range{ + Start: protocol.Position{ + Line: position.Line, + Character: position.Character - 1, + }, + End: protocol.Position{ + Line: position.Line, + Character: position.Character, + }, + }, + NewText: insertText, + } + } + + return item +} + +func hasOnlyLetter(s []rune) bool { + for _, c := range s { + if (c < 'a' || c > 'z') && (c < 'A' || c > 'Z') { + return false + } + } + return true +} +func hasOnlyLettersAndNumber(s []rune) bool { + for _, c := range s { + if (c < 'a' || c > 'z') && (c < 'A' || c > 'Z') && (c < '0' || c > '9') { + return false + } + } + return true } func typeToString(t ast.Node) string { From b8c4aa632a78e651536153647ae4f10027baa3f1 Mon Sep 17 00:00:00 2001 From: Duologic Date: Mon, 11 Mar 2024 09:57:06 +0100 Subject: [PATCH 3/4] fix: use upstream logic to validate identifier --- pkg/server/completion.go | 52 ++++++++++++++++++++++++++++------------ 1 file changed, 37 insertions(+), 15 deletions(-) diff --git a/pkg/server/completion.go b/pkg/server/completion.go index ff7997c..c27f059 100644 --- a/pkg/server/completion.go +++ b/pkg/server/completion.go @@ -164,15 +164,14 @@ func createCompletionItemsFromRanges(ranges []processing.ObjectRange, completion } func createCompletionItem(label, prefix string, kind protocol.CompletionItemKind, body ast.Node, position protocol.Position) protocol.CompletionItem { - runes := []rune(label) - mustQuoteLabel := !(hasOnlyLetter(runes[0:1]) && len(runes) > 1 && hasOnlyLettersAndNumber(runes[1:])) + mustNotQuoteLabel := IsValidIdentifier(label) insertText := label detail := label if prefix != "" { detail = prefix + "." + insertText } - if mustQuoteLabel { + if !mustNotQuoteLabel { insertText = "['" + label + "']" detail = prefix + insertText } @@ -199,7 +198,7 @@ func createCompletionItem(label, prefix string, kind protocol.CompletionItemKind } // Remove leading `.` character when quoting label - if mustQuoteLabel { + if !mustNotQuoteLabel { item.TextEdit = &protocol.TextEdit{ Range: protocol.Range{ Start: protocol.Position{ @@ -218,23 +217,46 @@ func createCompletionItem(label, prefix string, kind protocol.CompletionItemKind return item } -func hasOnlyLetter(s []rune) bool { - for _, c := range s { - if (c < 'a' || c > 'z') && (c < 'A' || c > 'Z') { - return false - } - } - return true +// Start - Copied from go-jsonnet/internal/parser/lexer.go + +func isUpper(r rune) bool { + return r >= 'A' && r <= 'Z' +} +func isLower(r rune) bool { + return r >= 'a' && r <= 'z' +} +func isNumber(r rune) bool { + return r >= '0' && r <= '9' } -func hasOnlyLettersAndNumber(s []rune) bool { - for _, c := range s { - if (c < 'a' || c > 'z') && (c < 'A' || c > 'Z') && (c < '0' || c > '9') { - return false +func isIdentifierFirst(r rune) bool { + return isUpper(r) || isLower(r) || r == '_' +} +func isIdentifier(r rune) bool { + return isIdentifierFirst(r) || isNumber(r) +} +func IsValidIdentifier(str string) bool { + if len(str) == 0 { + return false + } + for i, r := range str { + if i == 0 { + if !isIdentifierFirst(r) { + return false + } + } else { + if !isIdentifier(r) { + return false + } } } + // Ignore tokens for now, we should ask upstream to make the formatter a public package + // so we can use go-jsonnet/internal/formatter/pretty_field_names.go directly. + //return getTokenKindFromID(str) == tokenIdentifier return true } +// End - Copied from go-jsonnet/internal/parser/lexer.go + func typeToString(t ast.Node) string { switch t.(type) { case *ast.Array: From da14fad37d151d9ca116983691bb6a95fd45f167 Mon Sep 17 00:00:00 2001 From: Duologic Date: Mon, 11 Mar 2024 09:59:38 +0100 Subject: [PATCH 4/4] style --- pkg/server/completion.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/server/completion.go b/pkg/server/completion.go index c27f059..0060b2d 100644 --- a/pkg/server/completion.go +++ b/pkg/server/completion.go @@ -251,7 +251,7 @@ func IsValidIdentifier(str string) bool { } // Ignore tokens for now, we should ask upstream to make the formatter a public package // so we can use go-jsonnet/internal/formatter/pretty_field_names.go directly. - //return getTokenKindFromID(str) == tokenIdentifier + // return getTokenKindFromID(str) == tokenIdentifier return true }