diff --git a/internal/lsp/completions/providers/builtins.go b/internal/lsp/completions/providers/builtins.go index 6e300918..8f9ec996 100644 --- a/internal/lsp/completions/providers/builtins.go +++ b/internal/lsp/completions/providers/builtins.go @@ -1,8 +1,10 @@ package providers import ( + "fmt" "strings" + "github.com/open-policy-agent/opa/ast" "github.com/styrainc/regal/internal/lsp/cache" "github.com/styrainc/regal/internal/lsp/hover" "github.com/styrainc/regal/internal/lsp/rego" @@ -56,6 +58,8 @@ func (*BuiltIns) Run(c *cache.Cache, params types.CompletionParams, _ *Options) continue } + insertTextFormat := uint(2) // snippet + items = append(items, types.CompletionItem{ Label: key, Kind: completion.Function, @@ -64,6 +68,7 @@ func (*BuiltIns) Run(c *cache.Cache, params types.CompletionParams, _ *Options) Kind: "markdown", Value: hover.CreateHoverContent(builtIn), }, + InsertTextFormat: &insertTextFormat, TextEdit: &types.TextEdit{ Range: types.Range{ Start: types.Position{ @@ -75,10 +80,32 @@ func (*BuiltIns) Run(c *cache.Cache, params types.CompletionParams, _ *Options) Character: params.Position.Character, }, }, - NewText: key, + NewText: newTextForBuiltIn(builtIn), }, }) } return items, nil } + +func newTextForBuiltIn(bi *ast.Builtin) string { + args := make([]string, len(bi.Decl.Args())) + for i, arg := range bi.Decl.NamedFuncArgs().Args { + args[i] = strings.Split(arg.String(), ":")[0] + } + + if len(args) == 0 { + return bi.Name + "($0)" + } + + argString := "" + for i, arg := range args { + if i > 0 { + argString += ", " + } + + argString += fmt.Sprintf("${%d:%s}", i+1, arg) + } + + return fmt.Sprintf("%s(%s)", bi.Name, argString) +} diff --git a/internal/lsp/completions/providers/builtins_test.go b/internal/lsp/completions/providers/builtins_test.go index 40e1db3b..c5a240e6 100644 --- a/internal/lsp/completions/providers/builtins_test.go +++ b/internal/lsp/completions/providers/builtins_test.go @@ -6,9 +6,42 @@ import ( "testing" "github.com/styrainc/regal/internal/lsp/cache" + "github.com/styrainc/regal/internal/lsp/rego" "github.com/styrainc/regal/internal/lsp/types" ) +func TestNewTextForBuiltIn(t *testing.T) { + testCases := map[string]struct { + BuiltIn string + Output string + }{ + "print": { + BuiltIn: "print", + Output: "print($0)", + }, + "strings.count": { + BuiltIn: "strings.count", + Output: "strings.count(${1:search}, ${2:substring})", + }, + } + + bis := rego.GetBuiltins() + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + bi, ok := bis[tc.BuiltIn] + if !ok { + t.Fatalf("BuiltIn %s not found", tc.BuiltIn) + } + + output := newTextForBuiltIn(bi) + if output != tc.Output { + t.Fatalf("Expected\n%s\ngot\n%s", tc.Output, output) + } + }) + } +} + func TestBuiltIns_if(t *testing.T) { t.Parallel() diff --git a/internal/lsp/types/types.go b/internal/lsp/types/types.go index 1087a032..39957e68 100644 --- a/internal/lsp/types/types.go +++ b/internal/lsp/types/types.go @@ -137,12 +137,12 @@ type CompletionItem struct { Label string `json:"label"` LabelDetails *CompletionItemLabelDetails `json:"labelDetails,omitempty"` // https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#completionItemKind - Kind completion.ItemKind `json:"kind"` - Detail string `json:"detail"` - Documentation *MarkupContent `json:"documentation,omitempty"` - Preselect bool `json:"preselect"` - TextEdit *TextEdit `json:"textEdit,omitempty"` - InserTextFormat *uint `json:"insertTextFormat,omitempty"` + Kind completion.ItemKind `json:"kind"` + Detail string `json:"detail"` + Documentation *MarkupContent `json:"documentation,omitempty"` + Preselect bool `json:"preselect"` + TextEdit *TextEdit `json:"textEdit,omitempty"` + InsertTextFormat *uint `json:"insertTextFormat,omitempty"` // Mandatory is used to indicate that the completion item is mandatory and should be offered // as an exclusive completion. This is not part of the LSP spec, but used in regal providers