Skip to content

Commit 1e39652

Browse files
authored
Ajust suggestions/autocomplete for multiple clients/scenarios (#300)
1 parent 891922f commit 1e39652

File tree

4 files changed

+304
-98
lines changed

4 files changed

+304
-98
lines changed

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

Lines changed: 125 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -133,13 +133,14 @@ defmodule ElixirLS.LanguageServer.Providers.Completion do
133133

134134
items =
135135
ElixirSense.suggestions(text, line + 1, character + 1)
136+
|> maybe_reject_derived_functions(context, options)
136137
|> Enum.map(&from_completion_item(&1, context, options))
137138
|> Enum.concat(module_attr_snippets(context))
138139

139140
items_json =
140141
items
141142
|> Enum.reject(&is_nil/1)
142-
|> Enum.uniq_by(& &1.insert_text)
143+
|> Enum.uniq_by(&{&1.detail, &1.documentation, &1.insert_text})
143144
|> sort_items()
144145
|> items_to_json(options)
145146

@@ -159,6 +160,21 @@ defmodule ElixirLS.LanguageServer.Providers.Completion do
159160
end
160161
end
161162

163+
defp maybe_reject_derived_functions(suggestions, context, options) do
164+
locals_without_parens = Keyword.get(options, :locals_without_parens)
165+
signature_help_supported = Keyword.get(options, :signature_help_supported, false)
166+
capture_before? = context.capture_before?
167+
168+
Enum.reject(suggestions, fn s ->
169+
s.type in [:function, :macro] &&
170+
!capture_before? &&
171+
s.arity < s.def_arity &&
172+
signature_help_supported &&
173+
function_name_with_parens?(s.name, s.arity, locals_without_parens) &&
174+
function_name_with_parens?(s.name, s.def_arity, locals_without_parens)
175+
end)
176+
end
177+
162178
defp from_completion_item(
163179
%{type: :attribute, name: name},
164180
%{
@@ -288,7 +304,7 @@ defmodule ElixirLS.LanguageServer.Providers.Completion do
288304

289305
opts = Keyword.put(options, :with_parens?, true)
290306
insert_text = def_snippet(def_str, name, args, arity, opts)
291-
label = "#{def_str}#{function_label(name, args, arity)}"
307+
label = "#{def_str}#{name}/#{arity}"
292308

293309
filter_text =
294310
if def_str do
@@ -327,7 +343,7 @@ defmodule ElixirLS.LanguageServer.Providers.Completion do
327343
def_str = if(context[:def_before] == nil, do: "def ")
328344

329345
insert_text = def_snippet(def_str, name, args, arity, options)
330-
label = "#{def_str}#{function_label(name, args, arity)}"
346+
label = "#{def_str}#{name}/#{arity}"
331347

332348
%__MODULE__{
333349
label: label,
@@ -443,10 +459,6 @@ defmodule ElixirLS.LanguageServer.Providers.Completion do
443459
nil
444460
end
445461

446-
defp function_label(name, _args, arity) do
447-
Enum.join([to_string(name), "/", arity])
448-
end
449-
450462
defp def_snippet(def_str, name, args, arity, opts) do
451463
if Keyword.get(opts, :snippets_supported, false) do
452464
"#{def_str}#{function_snippet(name, args, arity, opts)} do\n\t$0\nend"
@@ -456,56 +468,93 @@ defmodule ElixirLS.LanguageServer.Providers.Completion do
456468
end
457469

458470
defp function_snippet(name, args, arity, opts) do
459-
cond do
460-
Keyword.get(opts, :capture_before?) && arity <= 1 ->
461-
Enum.join([name, "/", arity])
471+
snippets_supported? = Keyword.get(opts, :snippets_supported, false)
472+
trigger_signature? = Keyword.get(opts, :trigger_signature?, false)
473+
capture_before? = Keyword.get(opts, :capture_before?, false)
462474

463-
not Keyword.get(opts, :snippets_supported, false) ->
464-
name
475+
cond do
476+
capture_before? ->
477+
function_snippet_with_capture_before(name, arity, snippets_supported?)
465478

466-
Keyword.get(opts, :trigger_signature?, false) ->
479+
trigger_signature? ->
467480
text_after_cursor = Keyword.get(opts, :text_after_cursor, "")
481+
function_snippet_with_signature(name, text_after_cursor, snippets_supported?)
468482

469-
# Don't add the closing parenthesis to the snippet if the cursor is
470-
# immediately before a valid argument (this usually happens when we
471-
# want to wrap an existing variable or literal, e.g. using IO.inspect)
472-
if Regex.match?(~r/^[a-zA-Z0-9_:"'%<\[\{]/, text_after_cursor) do
473-
"#{name}("
474-
else
475-
"#{name}($1)$0"
476-
end
483+
has_text_after_cursor?(opts) ->
484+
name
485+
486+
snippets_supported? ->
487+
pipe_before? = Keyword.get(opts, :pipe_before?, false)
488+
with_parens? = Keyword.get(opts, :with_parens?, false)
489+
function_snippet_with_args(name, arity, args, pipe_before?, with_parens?)
477490

478491
true ->
479-
args_list =
480-
if args && args != "" do
481-
split_args(args)
482-
else
483-
for i <- Enum.slice(0..arity, 1..-1), do: "arg#{i}"
484-
end
492+
name
493+
end
494+
end
485495

486-
args_list =
487-
if Keyword.get(opts, :pipe_before?) do
488-
Enum.slice(args_list, 1..-1)
489-
else
490-
args_list
491-
end
496+
defp function_snippet_with_args(name, arity, args, pipe_before?, with_parens?) do
497+
args_list =
498+
if args && args != "" do
499+
split_args_for_snippet(args, arity)
500+
else
501+
for i <- Enum.slice(0..arity, 1..-1), do: "arg#{i}"
502+
end
492503

493-
tabstops =
494-
args_list
495-
|> Enum.with_index()
496-
|> Enum.map(fn {arg, i} -> "${#{i + 1}:#{arg}}" end)
504+
args_list =
505+
if pipe_before? do
506+
Enum.slice(args_list, 1..-1)
507+
else
508+
args_list
509+
end
497510

498-
{before_args, after_args} =
499-
if Keyword.get(opts, :with_parens?, false) do
500-
{"(", ")"}
501-
else
502-
{" ", ""}
503-
end
511+
tabstops =
512+
args_list
513+
|> Enum.with_index()
514+
|> Enum.map(fn {arg, i} -> "${#{i + 1}:#{arg}}" end)
504515

505-
Enum.join([name, before_args, Enum.join(tabstops, ", "), after_args])
516+
{before_args, after_args} =
517+
if with_parens? do
518+
{"(", ")"}
519+
else
520+
{" ", ""}
521+
end
522+
523+
Enum.join([name, before_args, Enum.join(tabstops, ", "), after_args])
524+
end
525+
526+
defp function_snippet_with_signature(name, text_after_cursor, snippets_supported?) do
527+
# Don't add the closing parenthesis to the snippet if the cursor is
528+
# immediately before a valid argument. This usually happens when we
529+
# want to wrap an existing variable or literal, e.g. using IO.inspect/2.
530+
if !snippets_supported? || Regex.match?(~r/^[a-zA-Z0-9_:"'%<@\[\{]/, text_after_cursor) do
531+
"#{name}("
532+
else
533+
"#{name}($1)$0"
506534
end
507535
end
508536

537+
defp function_snippet_with_capture_before(name, 0, _snippets_supported?) do
538+
"#{name}/0"
539+
end
540+
541+
defp function_snippet_with_capture_before(name, arity, snippets_supported?) do
542+
if snippets_supported? do
543+
"#{name}${1:/#{arity}}$0"
544+
else
545+
"#{name}/#{arity}"
546+
end
547+
end
548+
549+
defp has_text_after_cursor?(opts) do
550+
text =
551+
opts
552+
|> Keyword.get(:text_after_cursor, "")
553+
|> String.trim()
554+
555+
text != ""
556+
end
557+
509558
defp completion_kind(type) do
510559
case type do
511560
:text -> 1
@@ -545,17 +594,34 @@ defmodule ElixirLS.LanguageServer.Providers.Completion do
545594
end
546595
end
547596

548-
defp split_args(args) do
597+
defp split_args_for_snippet(args, arity) do
549598
args
550599
|> String.replace("\\", "\\\\")
551600
|> String.replace("$", "\\$")
552601
|> String.replace("}", "\\}")
553602
|> String.split(",")
554-
|> Enum.reject(&is_default_argument?/1)
555-
|> Enum.map(&String.trim/1)
603+
|> remove_unused_default_args(arity)
556604
end
557605

558-
defp is_default_argument?(s), do: String.contains?(s, "\\\\")
606+
defp remove_unused_default_args(args, arity) do
607+
reversed_args = Enum.reverse(args)
608+
acc = {[], length(args) - arity}
609+
610+
{result, _} =
611+
Enum.reduce(reversed_args, acc, fn arg, {result, remove_count} ->
612+
parts = String.split(arg, "\\\\\\\\")
613+
var = Enum.at(parts, 0) |> String.trim()
614+
default_value = Enum.at(parts, 1)
615+
616+
if remove_count > 0 && default_value do
617+
{result, remove_count - 1}
618+
else
619+
{[var | result], remove_count}
620+
end
621+
end)
622+
623+
result
624+
end
559625

560626
defp module_attr_snippets(%{prefix: prefix, scope: :module, def_before: nil}) do
561627
for {name, snippet, docs} <- @module_attr_snippets,
@@ -585,7 +651,6 @@ defmodule ElixirLS.LanguageServer.Providers.Completion do
585651
defp function_completion(info, context, options) do
586652
%{
587653
type: type,
588-
visibility: visibility,
589654
args: args,
590655
name: name,
591656
summary: summary,
@@ -606,13 +671,11 @@ defmodule ElixirLS.LanguageServer.Providers.Completion do
606671
} = context
607672

608673
locals_without_parens = Keyword.get(options, :locals_without_parens)
674+
signature_help_supported? = Keyword.get(options, :signature_help_supported, false)
609675
with_parens? = function_name_with_parens?(name, arity, locals_without_parens)
610676

611677
trigger_signature? =
612-
Keyword.get(options, :signature_help_supported, false) &&
613-
Keyword.get(options, :snippets_supported, false) &&
614-
arity > 0 &&
615-
with_parens?
678+
signature_help_supported? && with_parens? && ((arity == 1 && !pipe_before?) || arity > 1)
616679

617680
{label, insert_text} =
618681
cond do
@@ -625,7 +688,7 @@ defmodule ElixirLS.LanguageServer.Providers.Completion do
625688
{name, name}
626689

627690
true ->
628-
label = function_label(name, args, arity)
691+
label = "#{name}/#{arity}"
629692

630693
insert_text =
631694
function_snippet(
@@ -646,22 +709,19 @@ defmodule ElixirLS.LanguageServer.Providers.Completion do
646709
{label, insert_text}
647710
end
648711

649-
detail_header =
712+
detail_prefix =
650713
if inspect(module) == origin do
651-
"#{visibility} #{type}"
714+
"(#{type}) "
652715
else
653-
"#{origin} #{type}"
716+
"(#{type}) #{origin}."
654717
end
655718

656-
footer =
657-
if String.starts_with?(type, ["private", "public"]) do
658-
String.replace(type, "_", " ")
659-
else
660-
SourceFile.format_spec(spec, line_length: 30)
661-
end
719+
detail = Enum.join([detail_prefix, name, "(", args, ")"])
720+
721+
footer = SourceFile.format_spec(spec, line_length: 30)
662722

663723
command =
664-
if trigger_signature? do
724+
if trigger_signature? && !capture_before? do
665725
%{
666726
"title" => "Trigger Parameter Hint",
667727
"command" => "editor.action.triggerParameterHints"
@@ -671,7 +731,7 @@ defmodule ElixirLS.LanguageServer.Providers.Completion do
671731
%__MODULE__{
672732
label: label,
673733
kind: :function,
674-
detail: detail_header <> "\n\n" <> Enum.join([to_string(name), "(", args, ")"]),
734+
detail: detail,
675735
documentation: summary <> footer,
676736
insert_text: insert_text,
677737
priority: 7,

0 commit comments

Comments
 (0)