Skip to content

Commit 4367692

Browse files
authored
Refactor: Make callable completions consistent (lexical-lsp#156)
We spent a bunch of time working on function completions in a variety of cases, but macro completions still were quite primitive, only completing with the name and arity, which is almost never what you want. This refactors the logic for building these completions into a `Callable` module so both macros and functions can share their behavior. Fixes lexical-lsp#136
1 parent ec4cc9d commit 4367692

File tree

5 files changed

+163
-90
lines changed

5 files changed

+163
-90
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
defmodule Project.Macros do
2+
defmacro macro_add(a, b) do
3+
quote do
4+
unquote(a) + unquote(b)
5+
end
6+
end
7+
8+
defmacro l(message) do
9+
quote do
10+
require Logger
11+
Logger.info("message is: #{unquote(inspect(message))}")
12+
end
13+
end
14+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
defmodule Lexical.Server.CodeIntelligence.Completion.Translations.Callable do
2+
alias Lexical.RemoteControl.Completion.Result
3+
alias Lexical.Server.CodeIntelligence.Completion.Env
4+
5+
@callables [Result.Function, Result.Macro]
6+
7+
def completion(%callable_module{} = callable, %Env{} = env)
8+
when callable_module in @callables do
9+
add_args? = not String.contains?(env.suffix, "(")
10+
11+
insert_text =
12+
if add_args? do
13+
callable_snippet(callable, env)
14+
else
15+
callable.name
16+
end
17+
18+
tags =
19+
if Map.get(callable.metadata, :deprecated) do
20+
[:deprecated]
21+
end
22+
23+
Env.snippet(env, insert_text,
24+
kind: :function,
25+
label: label(callable),
26+
sort_text: sort_text(callable),
27+
tags: tags
28+
)
29+
end
30+
31+
def capture_completions(%callable_module{} = callable, %Env{} = env)
32+
when callable_module in @callables do
33+
name_and_arity = name_and_arity(callable)
34+
35+
complete_capture =
36+
Env.plain_text(env, name_and_arity,
37+
detail: "(Capture)",
38+
kind: :function,
39+
label: name_and_arity,
40+
sort_text: "&" <> sort_text(callable)
41+
)
42+
43+
call_capture =
44+
Env.snippet(env, callable_snippet(callable, env),
45+
detail: "(Capture with arguments)",
46+
kind: :function,
47+
label: label(callable),
48+
sort_text: "&" <> sort_text(callable)
49+
)
50+
51+
[complete_capture, call_capture]
52+
end
53+
54+
defp callable_snippet(%_{} = callable, %Env{} = env) do
55+
argument_names =
56+
if Env.pipe?(env) do
57+
tl(callable.argument_names)
58+
else
59+
callable.argument_names
60+
end
61+
62+
argument_templates =
63+
argument_names
64+
|> Enum.with_index()
65+
|> Enum.map_join(", ", fn {name, index} ->
66+
escaped_name = String.replace(name, "\\", "\\\\")
67+
"${#{index + 1}:#{escaped_name}}"
68+
end)
69+
70+
"#{callable.name}(#{argument_templates})"
71+
end
72+
73+
defp sort_text(%_{name: name, arity: arity}) do
74+
normalized = String.replace(name, "__", "")
75+
"#{normalized}/#{arity}"
76+
end
77+
78+
defp label(%_{name: name, argument_names: argument_names}) do
79+
arg_detail = Enum.join(argument_names, ", ")
80+
"#{name}(#{arg_detail})"
81+
end
82+
83+
defp name_and_arity(%_{name: name, arity: arity}) do
84+
"#{name}/#{arity}"
85+
end
86+
end

apps/server/lib/lexical/server/code_intelligence/completion/translations/function.ex

+3-80
Original file line numberDiff line numberDiff line change
@@ -2,92 +2,15 @@ defmodule Lexical.Server.CodeIntelligence.Completion.Translations.Function do
22
alias Lexical.RemoteControl.Completion.Result
33
alias Lexical.Server.CodeIntelligence.Completion.Env
44
alias Lexical.Server.CodeIntelligence.Completion.Translatable
5+
alias Lexical.Server.CodeIntelligence.Completion.Translations.Callable
56

67
use Translatable.Impl, for: Result.Function
78

89
def translate(%Result.Function{} = function, _builder, %Env{} = env) do
910
if Env.function_capture?(env) do
10-
function_capture_completions(function, env)
11+
Callable.capture_completions(function, env)
1112
else
12-
function_call_completion(function, env)
13+
Callable.completion(function, env)
1314
end
1415
end
15-
16-
defp function_capture_completions(%Result.Function{} = function, %Env{} = env) do
17-
name_and_arity = name_and_arity(function)
18-
19-
complete_capture =
20-
Env.plain_text(env, name_and_arity,
21-
detail: "(Capture)",
22-
kind: :function,
23-
label: name_and_arity,
24-
sort_text: "&" <> sort_text(function)
25-
)
26-
27-
call_capture =
28-
Env.snippet(env, function_snippet(function, env),
29-
detail: "(Capture with arguments)",
30-
kind: :function,
31-
label: function_label(function),
32-
sort_text: "&" <> sort_text(function)
33-
)
34-
35-
[complete_capture, call_capture]
36-
end
37-
38-
defp function_call_completion(%Result.Function{} = function, %Env{} = env) do
39-
add_args? = not String.contains?(env.suffix, "(")
40-
41-
insert_text =
42-
if add_args? do
43-
function_snippet(function, env)
44-
else
45-
function.name
46-
end
47-
48-
tags =
49-
if Map.get(function.metadata, :deprecated) do
50-
[:deprecated]
51-
end
52-
53-
Env.snippet(env, insert_text,
54-
kind: :function,
55-
label: function_label(function),
56-
sort_text: sort_text(function),
57-
tags: tags
58-
)
59-
end
60-
61-
defp function_snippet(%Result.Function{} = function, %Env{} = env) do
62-
argument_names =
63-
if Env.pipe?(env) do
64-
tl(function.argument_names)
65-
else
66-
function.argument_names
67-
end
68-
69-
argument_templates =
70-
argument_names
71-
|> Enum.with_index()
72-
|> Enum.map_join(", ", fn {name, index} ->
73-
escaped_name = String.replace(name, "\\", "\\\\")
74-
"${#{index + 1}:#{escaped_name}}"
75-
end)
76-
77-
"#{function.name}(#{argument_templates})"
78-
end
79-
80-
defp sort_text(%Result.Function{} = function) do
81-
normalized = String.replace(function.name, "__", "")
82-
"#{normalized}/#{function.arity}"
83-
end
84-
85-
defp function_label(%Result.Function{} = function) do
86-
arg_detail = Enum.join(function.argument_names, ", ")
87-
"#{function.name}(#{arg_detail})"
88-
end
89-
90-
defp name_and_arity(%Result.Function{} = function) do
91-
"#{function.name}/#{function.arity}"
92-
end
9316
end

apps/server/lib/lexical/server/code_intelligence/completion/translations/macro.ex

+3-10
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ defmodule Lexical.Server.CodeIntelligence.Completion.Translations.Macro do
22
alias Lexical.RemoteControl.Completion.Result
33
alias Lexical.Server.CodeIntelligence.Completion.Env
44
alias Lexical.Server.CodeIntelligence.Completion.Translatable
5+
alias Lexical.Server.CodeIntelligence.Completion.Translations.Callable
56

67
use Translatable.Impl, for: Result.Macro
78

@@ -444,17 +445,9 @@ defmodule Lexical.Server.CodeIntelligence.Completion.Translations.Macro do
444445
:skip
445446
end
446447

447-
def translate(%Result.Macro{name: name} = macro, builder, env)
448+
def translate(%Result.Macro{name: name} = macro, _builder, env)
448449
when name not in @snippet_macros do
449-
label = "#{macro.name}/#{macro.arity}"
450-
sort_text = String.replace(label, "__", "")
451-
452-
builder.plain_text(env, label,
453-
detail: macro.spec,
454-
kind: :function,
455-
sort_text: sort_text,
456-
label: label
457-
)
450+
Callable.completion(macro, env)
458451
end
459452

460453
def translate(%Result.Macro{}, _builder, _env) do

apps/server/test/lexical/server/code_intelligence/completion/translations/macro_test.exs

+57
Original file line numberDiff line numberDiff line change
@@ -545,6 +545,63 @@ defmodule Lexical.Server.CodeIntelligence.Completion.Translations.MacroTest do
545545
end
546546
end
547547

548+
describe "normal macro completion" do
549+
test "completes imported macros", %{project: project} do
550+
source = ~q[
551+
import Project.Macros
552+
553+
macro_a|
554+
]
555+
556+
assert {:ok, completion} =
557+
project
558+
|> complete(source)
559+
|> fetch_completion(kind: :function)
560+
561+
assert completion.kind == :function
562+
assert completion.insert_text_format == :snippet
563+
assert completion.label == "macro_add(a, b)"
564+
assert completion.insert_text == "macro_add(${1:a}, ${2:b})"
565+
end
566+
567+
test "completes required macros", %{project: project} do
568+
source = ~q[
569+
require Project.Macros
570+
571+
Project.Macros.macro_a|
572+
]
573+
574+
assert {:ok, completion} =
575+
project
576+
|> complete(source)
577+
|> fetch_completion(kind: :function)
578+
579+
assert completion.kind == :function
580+
assert completion.insert_text_format == :snippet
581+
assert completion.label == "macro_add(a, b)"
582+
assert completion.insert_text == "macro_add(${1:a}, ${2:b})"
583+
end
584+
585+
test "completes aliased macros", %{project: project} do
586+
source = ~q[
587+
alias Project.Macros
588+
require Macros
589+
590+
Macros.macro_a|
591+
]
592+
593+
assert {:ok, completion} =
594+
project
595+
|> complete(source)
596+
|> fetch_completion(kind: :function)
597+
598+
assert completion.kind == :function
599+
assert completion.insert_text_format == :snippet
600+
assert completion.label == "macro_add(a, b)"
601+
assert completion.insert_text == "macro_add(${1:a}, ${2:b})"
602+
end
603+
end
604+
548605
describe "sort_text" do
549606
test "dunder macros have the dunder removed in their sort_text", %{project: project} do
550607
assert {:ok, completion} =

0 commit comments

Comments
 (0)