Skip to content

Commit 682c28d

Browse files
authored
Add generic completion item (#333)
* Support :generic suggestions * Add missing icons/kinds * Increase :priority of all non-generic types (allows ElixirSense to define suggestions with higher priorities, if needed) * Remove docs snippets (their are now provided by ElixirSense as :generic) * Update ElixirSense * Use signatureAfterComplete setting
1 parent c03cfa1 commit 682c28d

File tree

4 files changed

+142
-105
lines changed

4 files changed

+142
-105
lines changed

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

Lines changed: 82 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -23,16 +23,6 @@ defmodule ElixirLS.LanguageServer.Providers.Completion do
2323
:command
2424
]
2525

26-
# Format:
27-
# {name, snippet, documentation, priority}
28-
@module_attr_snippets [
29-
{~s(doc """"""), ~s(doc """\n$0\n"""), "Documents a function", 3},
30-
{"doc false", "doc false", "Marks this function as internal", 5},
31-
{~s(moduledoc """"""), ~s(moduledoc """\n$0\n"""), "Documents a module", 3},
32-
{"moduledoc false", "moduledoc false", "Marks this module as internal", 5},
33-
{"typedoc", ~s(typedoc """\n$0\n"""), "Documents a type specification", 3}
34-
]
35-
3626
@func_snippets %{
3727
{"Kernel.SpecialForms", "case"} => "case $1 do\n\t$2 ->\n\t\t$0\nend",
3828
{"Kernel.SpecialForms", "with"} => "with $2 <- $1 do\n\t$0\nend",
@@ -140,7 +130,6 @@ defmodule ElixirLS.LanguageServer.Providers.Completion do
140130
ElixirSense.suggestions(text, line + 1, character + 1)
141131
|> maybe_reject_derived_functions(context, options)
142132
|> Enum.map(&from_completion_item(&1, context, options))
143-
|> Enum.concat(module_attr_snippets(context))
144133

145134
items_json =
146135
items
@@ -171,10 +160,12 @@ defmodule ElixirLS.LanguageServer.Providers.Completion do
171160
capture_before? = context.capture_before?
172161

173162
Enum.reject(suggestions, fn s ->
174-
s.type in [:function, :macro] && !capture_before? && s.arity < s.def_arity &&
175-
signature_help_supported &&
176-
function_name_with_parens?(s.name, s.arity, locals_without_parens) &&
177-
function_name_with_parens?(s.name, s.def_arity, locals_without_parens)
163+
s.type in [:function, :macro] and
164+
!capture_before? and
165+
s.arity < s.def_arity and
166+
signature_help_supported and
167+
function_name_with_parens?(s.name, s.arity, locals_without_parens) ==
168+
function_name_with_parens?(s.name, s.def_arity, locals_without_parens)
178169
end)
179170
end
180171

@@ -200,7 +191,7 @@ defmodule ElixirLS.LanguageServer.Providers.Completion do
200191
detail: "module attribute",
201192
insert_text: insert_text,
202193
filter_text: name_only,
203-
priority: 4,
194+
priority: 14,
204195
tags: []
205196
}
206197
end
@@ -220,7 +211,7 @@ defmodule ElixirLS.LanguageServer.Providers.Completion do
220211
kind: :variable,
221212
detail: "variable",
222213
insert_text: name,
223-
priority: 3,
214+
priority: 13,
224215
tags: []
225216
}
226217
end
@@ -238,7 +229,7 @@ defmodule ElixirLS.LanguageServer.Providers.Completion do
238229
detail: "return value",
239230
documentation: spec,
240231
insert_text: snippet,
241-
priority: 5,
232+
priority: 15,
242233
tags: []
243234
}
244235
end
@@ -272,7 +263,7 @@ defmodule ElixirLS.LanguageServer.Providers.Completion do
272263
documentation: summary,
273264
insert_text: name,
274265
filter_text: name,
275-
priority: 4,
266+
priority: 14,
276267
tags: metadata_to_tags(metadata)
277268
}
278269
end
@@ -322,7 +313,7 @@ defmodule ElixirLS.LanguageServer.Providers.Completion do
322313
detail: "#{origin} callback",
323314
documentation: summary,
324315
insert_text: insert_text,
325-
priority: 2,
316+
priority: 12,
326317
filter_text: filter_text,
327318
tags: metadata_to_tags(metadata)
328319
}
@@ -354,7 +345,7 @@ defmodule ElixirLS.LanguageServer.Providers.Completion do
354345
detail: "#{origin} protocol function",
355346
documentation: summary,
356347
insert_text: insert_text,
357-
priority: 2,
348+
priority: 12,
358349
filter_text: name,
359350
tags: metadata_to_tags(metadata)
360351
}
@@ -376,7 +367,7 @@ defmodule ElixirLS.LanguageServer.Providers.Completion do
376367
label: to_string(name),
377368
detail: detail,
378369
insert_text: if(call?, do: name, else: "#{name}: "),
379-
priority: 0,
370+
priority: 10,
380371
kind: :field,
381372
tags: []
382373
}
@@ -398,7 +389,7 @@ defmodule ElixirLS.LanguageServer.Providers.Completion do
398389
detail: "#{type_spec}",
399390
documentation: "#{doc}#{formatted_spec}",
400391
insert_text: "#{name}: ",
401-
priority: 0,
392+
priority: 10,
402393
kind: :field,
403394
tags: []
404395
}
@@ -431,12 +422,38 @@ defmodule ElixirLS.LanguageServer.Providers.Completion do
431422
detail: "typespec #{signature}",
432423
documentation: "#{doc}#{formatted_spec}",
433424
insert_text: snippet,
434-
priority: 0,
425+
priority: 10,
435426
kind: :class,
436427
tags: metadata_to_tags(metadata)
437428
}
438429
end
439430

431+
defp from_completion_item(%{type: :generic, kind: kind, label: label} = suggestion, _ctx, opts) do
432+
insert_text =
433+
cond do
434+
suggestion[:snippet] && Keyword.get(opts, :snippets_supported, false) ->
435+
suggestion[:snippet]
436+
437+
insert_text = suggestion[:insert_text] ->
438+
insert_text
439+
440+
true ->
441+
label
442+
end
443+
444+
%__MODULE__{
445+
label: label,
446+
detail: suggestion[:detail] || "",
447+
documentation: suggestion[:documentation] || "",
448+
insert_text: insert_text,
449+
filter_text: suggestion[:filter_text],
450+
priority: suggestion[:priority] || 0,
451+
kind: kind,
452+
command: suggestion[:command],
453+
tags: []
454+
}
455+
end
456+
440457
defp from_completion_item(
441458
%{name: name, origin: origin} = item,
442459
%{def_before: nil} = context,
@@ -446,7 +463,7 @@ defmodule ElixirLS.LanguageServer.Providers.Completion do
446463

447464
completion =
448465
if origin == "Kernel" || origin == "Kernel.SpecialForms" do
449-
%{completion | kind: :keyword, priority: 8}
466+
%{completion | kind: :keyword, priority: 18}
450467
else
451468
completion
452469
end
@@ -474,21 +491,31 @@ defmodule ElixirLS.LanguageServer.Providers.Completion do
474491
snippets_supported? = Keyword.get(opts, :snippets_supported, false)
475492
trigger_signature? = Keyword.get(opts, :trigger_signature?, false)
476493
capture_before? = Keyword.get(opts, :capture_before?, false)
494+
pipe_before? = Keyword.get(opts, :pipe_before?, false)
495+
with_parens? = Keyword.get(opts, :with_parens?, false)
496+
snippet = Keyword.get(opts, :snippet)
477497

478498
cond do
499+
snippet && snippets_supported? && !pipe_before? && !capture_before? ->
500+
snippet
501+
479502
capture_before? ->
480503
function_snippet_with_capture_before(name, arity, snippets_supported?)
481504

482505
trigger_signature? ->
483506
text_after_cursor = Keyword.get(opts, :text_after_cursor, "")
484-
function_snippet_with_signature(name, text_after_cursor, snippets_supported?)
507+
508+
function_snippet_with_signature(
509+
name,
510+
text_after_cursor,
511+
snippets_supported?,
512+
with_parens?
513+
)
485514

486515
has_text_after_cursor?(opts) ->
487516
name
488517

489518
snippets_supported? ->
490-
pipe_before? = Keyword.get(opts, :pipe_before?, false)
491-
with_parens? = Keyword.get(opts, :with_parens?, false)
492519
function_snippet_with_args(name, arity, args, pipe_before?, with_parens?)
493520

494521
true ->
@@ -526,14 +553,19 @@ defmodule ElixirLS.LanguageServer.Providers.Completion do
526553
Enum.join([name, before_args, Enum.join(tabstops, ", "), after_args])
527554
end
528555

529-
defp function_snippet_with_signature(name, text_after_cursor, snippets_supported?) do
530-
# Don't add the closing parenthesis to the snippet if the cursor is
531-
# immediately before a valid argument. This usually happens when we
532-
# want to wrap an existing variable or literal, e.g. using IO.inspect/2.
533-
if !snippets_supported? || Regex.match?(~r/^[a-zA-Z0-9_:"'%<@\[\{]/, text_after_cursor) do
534-
"#{name}("
535-
else
536-
"#{name}($1)$0"
556+
defp function_snippet_with_signature(name, text_after_cursor, snippets_supported?, with_parens?) do
557+
cond do
558+
!with_parens? ->
559+
if String.starts_with?(text_after_cursor, " "), do: name, else: "#{name} "
560+
561+
# Don't add the closing parenthesis to the snippet if the cursor is
562+
# immediately before a valid argument. This usually happens when we
563+
# want to wrap an existing variable or literal, e.g. using IO.inspect/2.
564+
!snippets_supported? || Regex.match?(~r/^[a-zA-Z0-9_:"'%<@\[\{]/, text_after_cursor) ->
565+
"#{name}("
566+
567+
true ->
568+
"#{name}($1)$0"
537569
end
538570
end
539571

@@ -578,6 +610,13 @@ defmodule ElixirLS.LanguageServer.Providers.Completion do
578610
:color -> 16
579611
:file -> 17
580612
:reference -> 18
613+
:folder -> 19
614+
:enum_member -> 20
615+
:constant -> 21
616+
:struct -> 22
617+
:event -> 23
618+
:operator -> 24
619+
:type_parameter -> 25
581620
end
582621
end
583622

@@ -626,31 +665,6 @@ defmodule ElixirLS.LanguageServer.Providers.Completion do
626665
result
627666
end
628667

629-
defp module_attr_snippets(%{prefix: prefix, scope: :module, def_before: nil}) do
630-
for {name, snippet, docs, priority} <- @module_attr_snippets,
631-
label = "@" <> name,
632-
String.starts_with?(label, prefix) do
633-
snippet =
634-
case prefix do
635-
"@" <> _ -> snippet
636-
_ -> "@" <> snippet
637-
end
638-
639-
%__MODULE__{
640-
label: label,
641-
kind: :snippet,
642-
documentation: docs,
643-
detail: "module attribute snippet",
644-
insert_text: snippet,
645-
filter_text: name,
646-
tags: [],
647-
priority: priority
648-
}
649-
end
650-
end
651-
652-
defp module_attr_snippets(_), do: []
653-
654668
defp function_completion(info, context, options) do
655669
%{
656670
type: type,
@@ -675,10 +689,10 @@ defmodule ElixirLS.LanguageServer.Providers.Completion do
675689

676690
locals_without_parens = Keyword.get(options, :locals_without_parens)
677691
signature_help_supported? = Keyword.get(options, :signature_help_supported, false)
692+
signature_after_complete? = Keyword.get(options, :signature_after_complete, true)
678693
with_parens? = function_name_with_parens?(name, arity, locals_without_parens)
679694

680-
trigger_signature? =
681-
signature_help_supported? && with_parens? && ((arity == 1 && !pipe_before?) || arity > 1)
695+
trigger_signature? = signature_help_supported? && ((arity == 1 && !pipe_before?) || arity > 1)
682696

683697
{label, insert_text} =
684698
cond do
@@ -702,10 +716,12 @@ defmodule ElixirLS.LanguageServer.Providers.Completion do
702716
options,
703717
pipe_before?: pipe_before?,
704718
capture_before?: capture_before?,
719+
pipe_before?: pipe_before?,
705720
trigger_signature?: trigger_signature?,
706721
locals_without_parens: locals_without_parens,
707722
text_after_cursor: text_after_cursor,
708-
with_parens?: with_parens?
723+
with_parens?: with_parens?,
724+
snippet: info[:snippet]
709725
)
710726
)
711727

@@ -724,7 +740,7 @@ defmodule ElixirLS.LanguageServer.Providers.Completion do
724740
footer = SourceFile.format_spec(spec, line_length: 30)
725741

726742
command =
727-
if trigger_signature? && !capture_before? do
743+
if trigger_signature? && signature_after_complete? && !capture_before? do
728744
%{
729745
"title" => "Trigger Parameter Hint",
730746
"command" => "editor.action.triggerParameterHints"
@@ -737,7 +753,7 @@ defmodule ElixirLS.LanguageServer.Providers.Completion do
737753
detail: detail,
738754
documentation: summary <> footer,
739755
insert_text: insert_text,
740-
priority: 7,
756+
priority: 17,
741757
tags: metadata_to_tags(metadata),
742758
command: command
743759
}

apps/language_server/lib/language_server/server.ex

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -489,13 +489,16 @@ defmodule ElixirLS.LanguageServer.Server do
489489
end
490490
|> MapSet.new()
491491

492+
signature_after_complete = Map.get(state.settings || %{}, "signatureAfterComplete", true)
493+
492494
fun = fn ->
493495
Completion.completion(state.source_files[uri].text, line, character,
494496
snippets_supported: snippets_supported,
495497
deprecated_supported: deprecated_supported,
496498
tags_supported: tags_supported,
497499
signature_help_supported: signature_help_supported,
498-
locals_without_parens: locals_without_parens
500+
locals_without_parens: locals_without_parens,
501+
signature_after_complete: signature_after_complete
499502
)
500503
end
501504

0 commit comments

Comments
 (0)