Skip to content

Commit 17deaa4

Browse files
committed
make completions work for quoted remote calls
`Mymod."4quo\"ted"()` is valid elixir
1 parent 13849d1 commit 17deaa4

File tree

4 files changed

+109
-41
lines changed

4 files changed

+109
-41
lines changed

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
### Unreleased
22

3-
### v0.18.1: 27 December 2023
3+
### v0.18.1: 28 December 2023
44

55
#### Improvements
66

77
- Variables defined in `ex_unit` `test`, `setup` and `setup_all` context are now returned by completions provider. Navigation to variable definition and references now also works correctly
88
- Suggest spec code lens now emits specs for all arity variants when function has default arguments. Previously only the one with all parameters was emitted
99
- Missing required OTP `:crypto` module is now detected on startup
10+
- Completions provider is now properly returning quoted remote calls. Previously accepting a suggestion would insert invalid code
1011

1112
#### Fixes
1213

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

Lines changed: 63 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -695,7 +695,13 @@ defmodule ElixirLS.LanguageServer.Providers.Completion do
695695
insert_text =
696696
case name do
697697
name when name in ["size", "unit"] ->
698-
function_snippet(name, ["integer"], 1, options |> Keyword.merge(with_parens?: true))
698+
function_snippet(
699+
name,
700+
["integer"],
701+
1,
702+
"Kernel",
703+
options |> Keyword.merge(with_parens?: true)
704+
)
699705

700706
other ->
701707
other
@@ -895,7 +901,7 @@ defmodule ElixirLS.LanguageServer.Providers.Completion do
895901

896902
defp def_snippet(def_str, name, args, arity, opts) do
897903
if Keyword.get(opts, :snippets_supported, false) do
898-
"#{def_str}#{function_snippet(name, args, arity, opts)} do\n\t$0\nend"
904+
"#{def_str}#{function_snippet(name, args, arity, "Kernel", opts)} do\n\t$0\nend"
899905
else
900906
"#{def_str}#{name}"
901907
end
@@ -972,7 +978,8 @@ defmodule ElixirLS.LanguageServer.Providers.Completion do
972978
nil
973979
end
974980

975-
def function_snippet(name, args, arity, opts) do
981+
def function_snippet(name, args, arity, origin, opts) do
982+
name = sanitize_function_name(name, origin)
976983
snippets_supported? = Keyword.get(opts, :snippets_supported, false)
977984
trigger_signature? = Keyword.get(opts, :trigger_signature?, false)
978985
capture_before? = Keyword.get(opts, :capture_before?, false)
@@ -1173,36 +1180,32 @@ defmodule ElixirLS.LanguageServer.Providers.Completion do
11731180

11741181
trigger_signature? = signature_help_supported? && ((arity == 1 && !pipe_before?) || arity > 1)
11751182

1176-
{label, insert_text} =
1183+
insert_text =
11771184
cond do
11781185
match?("~" <> _, name) ->
11791186
"~" <> sigil_name = name
1180-
{name, sigil_name}
1187+
sigil_name
11811188

11821189
use_name_only?(origin, name) or String.starts_with?(text_after_cursor, "(") ->
1183-
{name, name}
1190+
sanitize_function_name(name, origin)
11841191

11851192
true ->
1186-
label = name
1187-
1188-
insert_text =
1189-
function_snippet(
1190-
name,
1191-
args_list,
1192-
arity,
1193-
Keyword.merge(
1194-
options,
1195-
pipe_before?: pipe_before?,
1196-
capture_before?: capture_before?,
1197-
trigger_signature?: trigger_signature?,
1198-
locals_without_parens: locals_without_parens,
1199-
text_after_cursor: text_after_cursor,
1200-
with_parens?: with_parens?,
1201-
snippet: info[:snippet]
1202-
)
1193+
function_snippet(
1194+
name,
1195+
args_list,
1196+
arity,
1197+
origin,
1198+
Keyword.merge(
1199+
options,
1200+
pipe_before?: pipe_before?,
1201+
capture_before?: capture_before?,
1202+
trigger_signature?: trigger_signature?,
1203+
locals_without_parens: locals_without_parens,
1204+
text_after_cursor: text_after_cursor,
1205+
with_parens?: with_parens?,
1206+
snippet: info[:snippet]
12031207
)
1204-
1205-
{label, insert_text}
1208+
)
12061209
end
12071210

12081211
footer = SourceFile.format_spec(spec, line_length: 30)
@@ -1215,20 +1218,24 @@ defmodule ElixirLS.LanguageServer.Providers.Completion do
12151218
}
12161219
end
12171220

1218-
%__MODULE__{
1219-
label: label,
1220-
kind: :function,
1221-
detail: to_string(type),
1222-
label_details: %{
1223-
"detail" => "(#{Enum.join(args_list, ", ")})",
1224-
"description" => "#{origin}.#{name}/#{arity}"
1225-
},
1226-
documentation: summary <> footer,
1227-
insert_text: insert_text,
1228-
priority: 17,
1229-
tags: metadata_to_tags(metadata),
1230-
command: command
1231-
}
1221+
label = sanitize_function_name(name, origin)
1222+
1223+
if label == name or remote_calls? do
1224+
%__MODULE__{
1225+
label: label,
1226+
kind: :function,
1227+
detail: to_string(type),
1228+
label_details: %{
1229+
"detail" => "(#{Enum.join(args_list, ", ")})",
1230+
"description" => "#{origin}.#{label}/#{arity}"
1231+
},
1232+
documentation: summary <> footer,
1233+
insert_text: insert_text,
1234+
priority: 17,
1235+
tags: metadata_to_tags(metadata),
1236+
command: command
1237+
}
1238+
end
12321239
end
12331240

12341241
defp use_name_only?(module_name, function_name) do
@@ -1369,4 +1376,21 @@ defmodule ElixirLS.LanguageServer.Providers.Completion do
13691376
|> MapSet.member?({String.to_atom(name), arity})
13701377
|> Kernel.not()
13711378
end
1379+
1380+
defp sanitize_function_name(name, origin) when origin in ["Kernel", "Kernel.SpecialForms"],
1381+
do: name
1382+
1383+
defp sanitize_function_name(name, origin) do
1384+
if not Regex.match?(~r/^([_\p{Ll}\p{Lo}][\p{L}\p{N}_]*[?!]?)$/u, name) do
1385+
# not an allowed identifier - quote
1386+
escaped =
1387+
name
1388+
|> String.replace("\\", "\\\\")
1389+
|> String.replace("\"", "\\\"")
1390+
1391+
"\"" <> escaped <> "\""
1392+
else
1393+
name
1394+
end
1395+
end
13721396
end

apps/language_server/test/providers/completion_test.exs

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1442,6 +1442,34 @@ defmodule ElixirLS.LanguageServer.Providers.CompletionTest do
14421442
"insertText" => "defprotocol $1 do\n\t$0\nend"
14431443
} = first
14441444
end
1445+
1446+
test "will suggest remote quoted calls" do
1447+
text = """
1448+
alias ElixirLS.LanguageServer.Fixtures.ExampleQuotedDefs, as: Quoted
1449+
Quoted.
1450+
# ^
1451+
"""
1452+
1453+
{line, char} = {1, 7}
1454+
1455+
TestUtils.assert_has_cursor_char(text, line, char)
1456+
{line, char} = SourceFile.lsp_position_to_elixir(text, {line, char})
1457+
parser_context = ParserContextBuilder.from_string(text, {line, char})
1458+
1459+
assert {:ok, %{"items" => items}} =
1460+
Completion.completion(
1461+
parser_context,
1462+
line,
1463+
char,
1464+
@supports
1465+
)
1466+
1467+
assert item = Enum.find(items, fn item -> item["label"] == "\"0abc\\\"asd\"" end)
1468+
assert item["insertText"] == "\"0abc\\\"asd\"($1)$0"
1469+
1470+
assert item["labelDetails"]["description"] ==
1471+
"ElixirLS.LanguageServer.Fixtures.ExampleQuotedDefs.\"0abc\\\"asd\"/2"
1472+
end
14451473
end
14461474

14471475
describe "generic suggestions" do
@@ -1501,7 +1529,13 @@ defmodule ElixirLS.LanguageServer.Providers.CompletionTest do
15011529
]
15021530

15031531
assert "do_sth()" ==
1504-
Completion.function_snippet("do_sth", ["My.record(x: x0, y: y0)"], 1, opts)
1532+
Completion.function_snippet(
1533+
"do_sth",
1534+
["My.record(x: x0, y: y0)"],
1535+
1,
1536+
"Kernel",
1537+
opts
1538+
)
15051539
end
15061540
end
15071541

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
defmodule ElixirLS.LanguageServer.Fixtures.ExampleQuotedDefs do
2+
@doc """
3+
quoted def
4+
"""
5+
@spec unquote(:"0abc\"asd")(any, integer) :: :ok
6+
def unquote(:"0abc\"asd")(example, arg) do
7+
:ok
8+
end
9+
end

0 commit comments

Comments
 (0)