Skip to content
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
150 changes: 124 additions & 26 deletions internal/lsp/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -1535,7 +1535,7 @@ func (s *Server) Completion(ctx context.Context, params *protocol.CompletionPara
Kind: protocol.CompletionItemKindFunction,
Detail: fmt.Sprintf(":%s.%s/%d", erlModule, e.Function, e.Arity),
}
applySnippet(&item, e.Function, e.Arity, e.Params, inPipe, s.snippetSupport)
applySnippet(&item, e.Function, e.Arity, e.Params, "", inPipe, s.snippetSupport)
items = append(items, item)
}
}
Expand Down Expand Up @@ -1597,7 +1597,7 @@ func (s *Server) Completion(ctx context.Context, params *protocol.CompletionPara
"line": r.Line,
},
}
applySnippet(&item, r.Function, r.Arity, r.Params, inPipe, s.snippetSupport)
applySnippet(&item, r.Function, r.Arity, r.Params, r.Kind, inPipe, s.snippetSupport)
items = append(items, item)
}

Expand Down Expand Up @@ -1685,7 +1685,7 @@ func (s *Server) Completion(ctx context.Context, params *protocol.CompletionPara
Kind: kindToCompletionItemKind(bf.Kind),
Detail: bf.Kind,
}
applySnippet(&item, bf.Name, bf.Arity, bf.Params, inPipe, s.snippetSupport)
applySnippet(&item, bf.Name, bf.Arity, bf.Params, bf.Kind, inPipe, s.snippetSupport)
items = append(items, item)
}
}
Expand Down Expand Up @@ -1713,7 +1713,7 @@ func (s *Server) Completion(ctx context.Context, params *protocol.CompletionPara
"line": r.Line,
},
}
applySnippet(&item, r.Function, r.Arity, r.Params, inPipe, s.snippetSupport)
applySnippet(&item, r.Function, r.Arity, r.Params, r.Kind, inPipe, s.snippetSupport)
items = append(items, item)
}
}
Expand Down Expand Up @@ -1758,6 +1758,21 @@ func (s *Server) Completion(ctx context.Context, params *protocol.CompletionPara
}
}
}
// Plain keyword completions (e.g., "do", "end"). These don't
// expand to snippets; they just insert the keyword itself so
// that pressing Enter doesn't replace the keyword with a
// VS Code word-based suggestion.
for _, kw := range elixirKeywords {
if strings.HasPrefix(kw, funcPrefix) && !seen[kw] {
seen[kw] = true
items = append(items, protocol.CompletionItem{
Label: kw,
Kind: protocol.CompletionItemKindKeyword,
Detail: "keyword",
Preselect: true,
})
}
}
}

if len(items) == 0 {
Expand Down Expand Up @@ -2110,6 +2125,9 @@ func (s *Server) addCompletionsFromUsing(moduleName, funcPrefix string, seen map
if !strings.HasPrefix(funcName, funcPrefix) {
continue
}
if useSnippets && elixirFormSnippets[funcName] != "" {
continue
}
for _, d := range defs {
key := funcKey(funcName, d.arity)
if !seen[key] {
Expand All @@ -2123,7 +2141,7 @@ func (s *Server) addCompletionsFromUsing(moduleName, funcPrefix string, seen map
"line": d.line,
},
}
applySnippet(&item, funcName, d.arity, d.params, inPipe, useSnippets)
applySnippet(&item, funcName, d.arity, d.params, d.kind, inPipe, useSnippets)
*items = append(*items, item)
}
}
Expand All @@ -2137,6 +2155,9 @@ func (s *Server) addCompletionsFromUsing(moduleName, funcPrefix string, seen map
for _, r := range results {
key := funcKey(r.Function, r.Arity)
if strings.HasPrefix(r.Function, funcPrefix) && !seen[key] {
if useSnippets && elixirFormSnippets[r.Function] != "" {
continue
}
seen[key] = true
item := protocol.CompletionItem{
Label: r.Function,
Expand All @@ -2147,7 +2168,7 @@ func (s *Server) addCompletionsFromUsing(moduleName, funcPrefix string, seen map
"line": r.Line,
},
}
applySnippet(&item, r.Function, r.Arity, r.Params, inPipe, useSnippets)
applySnippet(&item, r.Function, r.Arity, r.Params, r.Kind, inPipe, useSnippets)
*items = append(*items, item)
}
}
Expand Down Expand Up @@ -2298,23 +2319,84 @@ func funcKey(name string, arity int) string {
return name + "/" + strconv.Itoa(arity)
}

// elixirKeywords are plain keyword completions (no snippet expansion).
// They prevent VS Code from falling back to word-based completions when the
// user types a keyword like "do" or "end" and presses Enter.
var elixirKeywords = []string{"do", "end"}

var elixirFormSnippets = map[string]string{
"for": "for ${1:pattern} <- ${2:enumerable} do\n\t$0\nend",
"with": "with ${1:pattern} <- ${2:expression} do\n\t$0\nend",
"case": "case ${1:expression} do\n\t${2:pattern} ->\n\t\t$0\nend",
"cond": "cond do\n\t${1:condition} ->\n\t\t$0\nend",
"if": "if ${1:condition} do\n\t$0\nend",
"unless": "unless ${1:condition} do\n\t$0\nend",
"receive": "receive do\n\t${1:pattern} ->\n\t\t$0\nend",
"try": "try do\n\t$0\nrescue\n\t${1:exception} ->\n\t\t${2:handler}\nend",
"quote": "quote do\n\t$0\nend",
"fn": "fn ${1:args} -> $0 end",
}

func applySnippet(item *protocol.CompletionItem, name string, arity int, params string, inPipe bool, useSnippets bool) {
"do": "do\n\t$0\nend",
"defmodule": "defmodule ${1:Name} do\n\t$0\nend",
"def": "def ${1:name}$2 do\n\t$0\nend",
"defp": "defp ${1:name}$2 do\n\t$0\nend",
"defmacro": "defmacro ${1:name}$2 do\n\t$0\nend",
"defmacrop": "defmacrop ${1:name}$2 do\n\t$0\nend",
"defstruct": "defstruct [${1:fields}]$0",
"defexception": "defexception [${1:fields}]$0",
"defprotocol": "defprotocol ${1:Name} do\n\t$0\nend",
"defimpl": "defimpl ${1:Protocol}, for: ${2:Type} do\n\t$0\nend",
"defdelegate": "defdelegate ${1:func}$2, to: ${3:module}$0",
"defguard": "defguard ${1:name}$2 when ${3:condition}$0",
"defguardp": "defguardp ${1:name}$2 when ${3:condition}$0",
"defoverridable": "defoverridable ${1:name}: ${2:arity}$0",
"for": "for ${1:pattern} <- ${2:enumerable} do\n\t$0\nend",
"with": "with ${1:pattern} <- ${2:expression} do\n\t$0\nend",
"case": "case ${1:expression} do\n\t${2:pattern} ->\n\t\t$0\nend",
"cond": "cond do\n\t${1:condition} ->\n\t\t$0\nend",
"if": "if ${1:condition} do\n\t$0\nelse\n\t${2}\nend",
"unless": "unless ${1:condition} do\n\t$0\nend",
"receive": "receive do\n\t${1:pattern} ->\n\t\t$0\nend",
"try": "try do\n\t$0\nrescue\n\t${1:exception} ->\n\t\t${2:handler}\nend",
"quote": "quote do\n\t$0\nend",
"fn": "fn ${1:args} -> $0 end",
}

// noParenFuncs are macros conventionally written without parentheses in Elixir,
// such as ExUnit's test/describe. When generating completion snippets or plain
// call text, these names produce `name arg1, arg2` instead of `name(arg1, arg2)`.
var noParenFuncs = map[string]bool{
"test": true,
"describe": true,
"assert": true,
"refute": true,
"assert_raise": true,
"assert_receive": true,
"assert_received": true,
"refute_receive": true,
"refute_received": true,
"setup": true,
"setup_all": true,
"catch_error": true,
"catch_exit": true,
"catch_throw": true,
}

// doBlockSnippets provides custom snippet templates for functions that take
// do/end blocks. These are applied when the function is in scope via the
// import/use-chain (unlike elixirFormSnippets which are global special forms).
var doBlockSnippets = map[string]string{
"test": "test \"${1:description}\" do\n\t$0\nend",
"describe": "describe \"${1:description}\" do\n\t$0\nend",
"setup": "setup do\n\t$0\nend",
"setup_all": "setup_all do\n\t$0\nend",
"assert_raise": "assert_raise ${1:exception} do\n\t$0\nend",
}

func applySnippet(item *protocol.CompletionItem, name string, arity int, params string, kind string, inPipe bool, useSnippets bool) {
item.Label = fmt.Sprintf("%s/%d", name, arity)
item.FilterText = name

// Check for a do-block snippet template first. These provide full
// do/end block structure (e.g. `test "..." do ... end`) and take
// priority over auto-generated arg lists.
if useSnippets && isMacroKind(kind) {
if tmpl, ok := doBlockSnippets[name]; ok {
item.InsertTextFormat = protocol.InsertTextFormatSnippet
item.InsertText = tmpl
return
}
Comment thread
cursor[bot] marked this conversation as resolved.
}

snippetArity := arity
snippetParams := params
paramStartIndex := 1
Expand All @@ -2330,9 +2412,13 @@ func applySnippet(item *protocol.CompletionItem, name string, arity int, params
}
}

noParen := noParenFuncs[name]

if !useSnippets {
if snippetArity > 0 {
item.InsertText = functionCallText(name, snippetArity, snippetParams, paramStartIndex)
item.InsertText = functionCallText(name, snippetArity, snippetParams, paramStartIndex, noParen)
} else if noParen {
item.InsertText = name
} else {
item.InsertText = name + "()"
}
Expand All @@ -2341,21 +2427,27 @@ func applySnippet(item *protocol.CompletionItem, name string, arity int, params

if snippetArity > 0 {
item.InsertTextFormat = protocol.InsertTextFormatSnippet
item.InsertText = functionSnippet(name, snippetArity, snippetParams, paramStartIndex)
item.InsertText = functionSnippet(name, snippetArity, snippetParams, paramStartIndex, noParen)
} else if noParen {
item.InsertText = name
} else {
item.InsertText = name + "()"
}
}

func functionSnippet(name string, arity int, params string, paramStartIndex int) string {
return buildCallText(name, arity, params, true, paramStartIndex)
func isMacroKind(kind string) bool {
return kind == "defmacro" || kind == "defmacrop"
}

func functionCallText(name string, arity int, params string, paramStartIndex int) string {
return buildCallText(name, arity, params, false, paramStartIndex)
func functionSnippet(name string, arity int, params string, paramStartIndex int, noParen bool) string {
return buildCallText(name, arity, params, true, paramStartIndex, noParen)
}

func buildCallText(name string, arity int, params string, snippet bool, paramStartIndex int) string {
func functionCallText(name string, arity int, params string, paramStartIndex int, noParen bool) string {
return buildCallText(name, arity, params, false, paramStartIndex, noParen)
}

func buildCallText(name string, arity int, params string, snippet bool, paramStartIndex int, noParen bool) string {
if paramStartIndex < 1 {
paramStartIndex = 1
}
Expand All @@ -2375,6 +2467,12 @@ func buildCallText(name string, arity int, params string, snippet bool, paramSta
args = append(args, paramName)
}
}
if noParen {
if snippet {
return name + " " + strings.Join(args, ", ") + "$0"
}
return name + " " + strings.Join(args, ", ")
}
call := name + "(" + strings.Join(args, ", ") + ")"
if snippet {
call += "$0"
Expand Down
Loading
Loading