Skip to content

Commit f69397f

Browse files
authored
Remove function argument snippets if snippets unsupported (#207) (#223)
Prior to this change, function completion always filled in function arguments with snippets. If snippets are not supported by the client, do not fill in any arguments, but only complete the name of the function. Additionally, tweak the snippet regex to recognize ${1:arg1} as a snippet.
1 parent 3d337ae commit f69397f

File tree

2 files changed

+118
-55
lines changed

2 files changed

+118
-55
lines changed

apps/language_server/lib/language_server/providers/completion.ex

Lines changed: 89 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ defmodule ElixirLS.LanguageServer.Providers.Completion do
135135

136136
items =
137137
ElixirSense.suggestions(text, line + 1, character + 1)
138-
|> Enum.map(&from_completion_item(&1, context))
138+
|> Enum.map(&from_completion_item(&1, context, options))
139139
|> Enum.concat(module_attr_snippets(context))
140140
|> Enum.concat(keyword_completions(context))
141141

@@ -151,12 +151,16 @@ defmodule ElixirLS.LanguageServer.Providers.Completion do
151151

152152
## Helpers
153153

154-
defp from_completion_item(%{type: :attribute, name: name}, %{
155-
prefix: prefix,
156-
def_before: nil,
157-
capture_before?: false,
158-
pipe_before?: false
159-
}) do
154+
defp from_completion_item(
155+
%{type: :attribute, name: name},
156+
%{
157+
prefix: prefix,
158+
def_before: nil,
159+
capture_before?: false,
160+
pipe_before?: false
161+
},
162+
_options
163+
) do
160164
name_only = String.trim_leading(name, "@")
161165
insert_text = if String.starts_with?(prefix, "@"), do: name_only, else: name
162166

@@ -175,11 +179,15 @@ defmodule ElixirLS.LanguageServer.Providers.Completion do
175179
end
176180
end
177181

178-
defp from_completion_item(%{type: :variable, name: name}, %{
179-
def_before: nil,
180-
pipe_before?: false,
181-
capture_before?: false
182-
}) do
182+
defp from_completion_item(
183+
%{type: :variable, name: name},
184+
%{
185+
def_before: nil,
186+
pipe_before?: false,
187+
capture_before?: false
188+
},
189+
_options
190+
) do
183191
%__MODULE__{
184192
label: to_string(name),
185193
kind: :variable,
@@ -192,7 +200,8 @@ defmodule ElixirLS.LanguageServer.Providers.Completion do
192200

193201
defp from_completion_item(
194202
%{type: :return, description: description, spec: spec, snippet: snippet},
195-
%{def_before: nil, capture_before?: false, pipe_before?: false}
203+
%{def_before: nil, capture_before?: false, pipe_before?: false},
204+
_options
196205
) do
197206
snippet = Regex.replace(Regex.recompile!(~r/"\$\{(.*)\}\$"/U), snippet, "${\\1}")
198207

@@ -212,7 +221,8 @@ defmodule ElixirLS.LanguageServer.Providers.Completion do
212221
%{
213222
def_before: nil,
214223
prefix: prefix
215-
}
224+
},
225+
_options
216226
) do
217227
capitalized? = String.first(name) == String.upcase(String.first(name))
218228

@@ -252,7 +262,8 @@ defmodule ElixirLS.LanguageServer.Providers.Completion do
252262
origin: origin,
253263
metadata: metadata
254264
},
255-
context
265+
context,
266+
options
256267
) do
257268
if (context[:def_before] == :def && String.starts_with?(spec, "@macrocallback")) ||
258269
(context[:def_before] == :defmacro && String.starts_with?(spec, "@callback")) do
@@ -267,15 +278,15 @@ defmodule ElixirLS.LanguageServer.Providers.Completion do
267278
end
268279
end
269280

270-
full_snippet = "#{def_str}#{snippet(name, args, arity)} do\n\t$0\nend"
281+
insert_text = def_snippet(def_str, name, args, arity, options)
271282
label = "#{def_str}#{function_label(name, args, arity)}"
272283

273284
%__MODULE__{
274285
label: label,
275286
kind: :interface,
276287
detail: "#{origin} callback",
277288
documentation: summary,
278-
insert_text: full_snippet,
289+
insert_text: insert_text,
279290
priority: 2,
280291
filter_text: name,
281292
tags: metadata_to_tags(metadata)
@@ -294,19 +305,20 @@ defmodule ElixirLS.LanguageServer.Providers.Completion do
294305
origin: origin,
295306
metadata: metadata
296307
},
297-
context
308+
context,
309+
options
298310
) do
299311
def_str = if(context[:def_before] == nil, do: "def ")
300312

301-
full_snippet = "#{def_str}#{snippet(name, args, arity)} do\n\t$0\nend"
313+
insert_text = def_snippet(def_str, name, args, arity, options)
302314
label = "#{def_str}#{function_label(name, args, arity)}"
303315

304316
%__MODULE__{
305317
label: label,
306318
kind: :interface,
307319
detail: "#{origin} protocol function",
308320
documentation: summary,
309-
insert_text: full_snippet,
321+
insert_text: insert_text,
310322
priority: 2,
311323
filter_text: name,
312324
tags: metadata_to_tags(metadata)
@@ -315,7 +327,8 @@ defmodule ElixirLS.LanguageServer.Providers.Completion do
315327

316328
defp from_completion_item(
317329
%{type: :field, subtype: subtype, name: name, origin: origin, call?: call?},
318-
_context
330+
_context,
331+
_options
319332
) do
320333
detail =
321334
case {subtype, origin} do
@@ -334,7 +347,7 @@ defmodule ElixirLS.LanguageServer.Providers.Completion do
334347
}
335348
end
336349

337-
defp from_completion_item(%{type: :param_option} = suggestion, _context) do
350+
defp from_completion_item(%{type: :param_option} = suggestion, _context, _options) do
338351
%{name: name, origin: _origin, doc: doc, type_spec: type_spec, expanded_spec: expanded_spec} =
339352
suggestion
340353

@@ -356,7 +369,11 @@ defmodule ElixirLS.LanguageServer.Providers.Completion do
356369
}
357370
end
358371

359-
defp from_completion_item(%{type: :type_spec, metadata: metadata} = suggestion, _context) do
372+
defp from_completion_item(
373+
%{type: :type_spec, metadata: metadata} = suggestion,
374+
_context,
375+
_options
376+
) do
360377
%{name: name, arity: arity, origin: _origin, doc: doc, signature: signature, spec: spec} =
361378
suggestion
362379

@@ -387,9 +404,10 @@ defmodule ElixirLS.LanguageServer.Providers.Completion do
387404

388405
defp from_completion_item(
389406
%{name: name, origin: origin} = item,
390-
%{def_before: nil} = context
407+
%{def_before: nil} = context,
408+
options
391409
) do
392-
completion = function_completion(item, context)
410+
completion = function_completion(item, context, options)
393411

394412
completion =
395413
if origin == "Kernel" || origin == "Kernel.SpecialForms" do
@@ -405,7 +423,7 @@ defmodule ElixirLS.LanguageServer.Providers.Completion do
405423
end
406424
end
407425

408-
defp from_completion_item(_suggestion, _context) do
426+
defp from_completion_item(_suggestion, _context, _options) do
409427
nil
410428
end
411429

@@ -417,30 +435,43 @@ defmodule ElixirLS.LanguageServer.Providers.Completion do
417435
end
418436
end
419437

420-
defp snippet(name, args, arity, opts \\ []) do
421-
if Keyword.get(opts, :capture_before?) && arity <= 1 do
422-
Enum.join([name, "/", arity])
438+
defp def_snippet(def_str, name, args, arity, opts) do
439+
if Keyword.get(opts, :snippets_supported, false) do
440+
"#{def_str}#{function_snippet(name, args, arity)} do\n\t$0\nend"
423441
else
424-
args_list =
425-
if args && args != "" do
426-
split_args(args)
427-
else
428-
for i <- Enum.slice(0..arity, 1..-1), do: "arg#{i}"
429-
end
442+
"#{def_str}#{name}"
443+
end
444+
end
430445

431-
args_list =
432-
if Keyword.get(opts, :pipe_before?) do
433-
Enum.slice(args_list, 1..-1)
434-
else
435-
args_list
436-
end
446+
defp function_snippet(name, args, arity, opts \\ []) do
447+
cond do
448+
Keyword.get(opts, :capture_before?) && arity <= 1 ->
449+
Enum.join([name, "/", arity])
437450

438-
tabstops =
439-
args_list
440-
|> Enum.with_index()
441-
|> Enum.map(fn {arg, i} -> "${#{i + 1}:#{arg}}" end)
451+
not Keyword.get(opts, :snippets_supported, false) ->
452+
name
442453

443-
Enum.join([name, "(", Enum.join(tabstops, ", "), ")"])
454+
true ->
455+
args_list =
456+
if args && args != "" do
457+
split_args(args)
458+
else
459+
for i <- Enum.slice(0..arity, 1..-1), do: "arg#{i}"
460+
end
461+
462+
args_list =
463+
if Keyword.get(opts, :pipe_before?) do
464+
Enum.slice(args_list, 1..-1)
465+
else
466+
args_list
467+
end
468+
469+
tabstops =
470+
args_list
471+
|> Enum.with_index()
472+
|> Enum.map(fn {arg, i} -> "${#{i + 1}:#{arg}}" end)
473+
474+
Enum.join([name, "(", Enum.join(tabstops, ", "), ")"])
444475
end
445476
end
446477

@@ -534,7 +565,7 @@ defmodule ElixirLS.LanguageServer.Providers.Completion do
534565
end)
535566
end
536567

537-
defp function_completion(info, context) do
568+
defp function_completion(info, context, options) do
538569
%{
539570
type: type,
540571
args: args,
@@ -555,7 +586,7 @@ defmodule ElixirLS.LanguageServer.Providers.Completion do
555586
text_after_cursor: text_after_cursor
556587
} = context
557588

558-
{label, snippet} =
589+
{label, insert_text} =
559590
cond do
560591
match?("sigil_" <> _, name) ->
561592
"sigil_" <> sigil_name = name
@@ -568,16 +599,19 @@ defmodule ElixirLS.LanguageServer.Providers.Completion do
568599
true ->
569600
label = function_label(name, args, arity)
570601

571-
snippet =
572-
snippet(
602+
insert_text =
603+
function_snippet(
573604
name,
574605
args,
575606
arity,
576-
pipe_before?: pipe_before?,
577-
capture_before?: capture_before?
607+
Keyword.merge(
608+
options,
609+
pipe_before?: pipe_before?,
610+
capture_before?: capture_before?
611+
)
578612
)
579613

580-
{label, snippet}
614+
{label, insert_text}
581615
end
582616

583617
detail =
@@ -597,7 +631,7 @@ defmodule ElixirLS.LanguageServer.Providers.Completion do
597631
kind: :function,
598632
detail: detail,
599633
documentation: summary,
600-
insert_text: snippet,
634+
insert_text: insert_text,
601635
priority: 7,
602636
tags: metadata_to_tags(metadata)
603637
}
@@ -670,7 +704,7 @@ defmodule ElixirLS.LanguageServer.Providers.Completion do
670704
end
671705

672706
defp snippet?(item) do
673-
item.kind == :snippet || String.match?(item.insert_text, ~r/\$\d/)
707+
item.kind == :snippet || String.match?(item.insert_text, ~r/\${?\d/)
674708
end
675709

676710
# As defined by CompletionItemTag in https://microsoft.github.io/language-server-protocol/specifications/specification-current/

apps/language_server/test/providers/completion_test.exs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,35 @@ defmodule ElixirLS.LanguageServer.Providers.CompletionTest do
8787
assert length(items) == 0
8888
end
8989

90+
test "function with snippets not supported does not have argument placeholders" do
91+
text = """
92+
defmodule MyModule do
93+
def add(a, b), do: a + b
94+
95+
def dummy_function() do
96+
ad
97+
# ^
98+
end
99+
end
100+
"""
101+
102+
{line, char} = {4, 6}
103+
TestUtils.assert_has_cursor_char(text, line, char)
104+
105+
{:ok, %{"items" => [item]}} = Completion.completion(text, line, char, @supports)
106+
assert item["insertText"] == "add(${1:a}, ${2:b})"
107+
108+
{:ok, %{"items" => [item]}} =
109+
Completion.completion(
110+
text,
111+
line,
112+
char,
113+
@supports |> Keyword.put(:snippets_supported, false)
114+
)
115+
116+
assert item["insertText"] == "add"
117+
end
118+
90119
test "provides completions for protocol functions" do
91120
text = """
92121
defimpl ElixirLS.LanguageServer.Fixtures.ExampleProtocol, for: MyModule do

0 commit comments

Comments
 (0)