Skip to content

Commit 306f512

Browse files
authored
feat(completions): imports, aliases, module attributes (#410)
This patch adds support for completion candidates for functions/macros imported via `import`, modules aliases via `alias`, module attributes, and any of the above when injected via a macro such as `use`. However, this is powered by new APIs and compiler changes that will be available in Elixir 1.17, so when completions are enabled, we will use a bundled 1.17 runtime of Elixir, instead of the Elixir in the user's path. This is a tradeoff, but I think one that is worthwhile in the name of progress and improving the language and ecosystem. Once completions exit experimental status, this means that Next LS will always run with a bundled copy of Elixir of Elixir unless the user's local copy is sufficiently new. This can be controlled via a setting. Related #45 Closes #360 Closes #334
1 parent 8d6bce1 commit 306f512

25 files changed

+1098
-368
lines changed

.formatter.exs

+1-4
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,7 @@
1212
plugins: [Styler],
1313
inputs: [
1414
".formatter.exs",
15-
"{config,lib,}/**/*.{ex,exs}",
16-
"test/next_ls_test.exs",
17-
"test/test_helper.exs",
18-
"test/next_ls/**/*.{ex,exs}",
15+
"{config,lib,test}/**/*.{ex,exs}",
1916
"priv/**/*.ex"
2017
]
2118
]

.mise.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,5 @@ KERL_BUILD_DOCS = "yes"
33

44
[tools]
55
erlang = "26.2.2"
6-
elixir = "ref:514615d0347cb9bb513faa44ae1e36406979e516"
6+
elixir = "ref:e3b6a91b173f7e836401a6a75c3906c26bd7fd39"
77
zig = "0.11.0"

.tool-versions

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
erlang 26.2.2
2-
elixir ref:514615d0347cb9bb513faa44ae1e36406979e516
2+
elixir ref:e3b6a91b173f7e836401a6a75c3906c26bd7fd39
3+
zig 0.11.0

flake.nix

+2-2
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,9 @@
2626
musl = lib.optionals nixpkgs.legacyPackages.${system}.stdenv.isLinux (builtins.fetchurl (nixpkgs.lib.attrsets.getAttrs ["url" "sha256"] musls.${system}));
2727
otp = (pkgs.beam.packagesWith beamPackages.erlang).extend (final: prev: {
2828
elixir_1_17 = prev.elixir_1_16.override {
29-
rev = "514615d0347cb9bb513faa44ae1e36406979e516";
29+
rev = "e3b6a91b173f7e836401a6a75c3906c26bd7fd39";
3030
# You can discover this using Trust On First Use by filling in `lib.fakeHash`
31-
sha256 = "sha256-lEnDgHi1sRg+3/JTnQJVo1qqSi0X2cNN4i9i9M95B2A=";
31+
sha256 = "sha256-RK0aMW7pz7kQtK9XXN1wVCBxKOJKdQD7I/53V8rWD04=";
3232
version = "1.17.0-dev";
3333
};
3434

lib/next_ls.ex

+106-44
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ defmodule NextLS do
5454
alias NextLS.Progress
5555
alias NextLS.Runtime
5656

57+
require NextLS.Runtime
58+
5759
def start_link(args) do
5860
{args, opts} =
5961
Keyword.split(args, [
@@ -63,7 +65,9 @@ defmodule NextLS do
6365
:runtime_task_supervisor,
6466
:dynamic_supervisor,
6567
:extensions,
66-
:registry
68+
:registry,
69+
:bundle_base,
70+
:mix_home
6771
])
6872

6973
GenLSP.start_link(__MODULE__, args, opts)
@@ -74,6 +78,8 @@ defmodule NextLS do
7478
task_supervisor = Keyword.fetch!(args, :task_supervisor)
7579
runtime_task_supervisor = Keyword.fetch!(args, :runtime_task_supervisor)
7680
dynamic_supervisor = Keyword.fetch!(args, :dynamic_supervisor)
81+
bundle_base = Keyword.get(args, :bundle_base, Path.expand("~/.cache/elixir-tools/nextls"))
82+
mixhome = Keyword.get(args, :mix_home, Path.expand("~/.mix"))
7783

7884
registry = Keyword.fetch!(args, :registry)
7985

@@ -83,6 +89,8 @@ defmodule NextLS do
8389
cache = Keyword.fetch!(args, :cache)
8490
{:ok, logger} = DynamicSupervisor.start_child(dynamic_supervisor, {NextLS.Logger, lsp: lsp})
8591

92+
NextLS.Runtime.BundledElixir.install(bundle_base, logger, mix_home: mixhome)
93+
8694
{:ok,
8795
assign(lsp,
8896
auto_update: Keyword.get(args, :auto_update, false),
@@ -588,13 +596,16 @@ defmodule NextLS do
588596
end)
589597
|> Enum.join("\n")
590598

591-
env =
599+
ast =
592600
spliced
593-
|> Spitfire.parse(literal_encoder: &{:ok, {:__literal__, &2, [&1]}})
601+
|> Spitfire.parse(literal_encoder: &{:ok, {:__block__, &2, [&1]}})
594602
|> then(fn
595603
{:ok, ast} -> ast
596604
{:error, ast, _} -> ast
597605
end)
606+
607+
env =
608+
ast
598609
|> NextLS.ASTHelpers.find_cursor()
599610
|> then(fn
600611
{:ok, cursor} ->
@@ -627,6 +638,23 @@ defmodule NextLS do
627638
dispatch(lsp.assigns.registry, :runtimes, fn entries ->
628639
[{wuri, result}] =
629640
for {runtime, %{uri: wuri}} <- entries, String.starts_with?(uri, wuri) do
641+
ast =
642+
spliced
643+
|> Spitfire.parse()
644+
|> then(fn
645+
{:ok, ast} -> ast
646+
{:error, ast, _} -> ast
647+
end)
648+
649+
{:ok, {_, _, _, macro_env}} = Runtime.expand(runtime, ast, Path.basename(uri))
650+
651+
env =
652+
env
653+
|> Map.put(:functions, macro_env.functions)
654+
|> Map.put(:macros, macro_env.macros)
655+
|> Map.put(:aliases, macro_env.aliases)
656+
|> Map.put(:attrs, macro_env.attrs)
657+
630658
{wuri,
631659
document_slice
632660
|> String.to_charlist()
@@ -652,7 +680,7 @@ defmodule NextLS do
652680
{"#{name}/#{symbol.arity}", GenLSP.Enumerations.CompletionItemKind.function(), symbol.docs}
653681

654682
:module ->
655-
{name, GenLSP.Enumerations.CompletionItemKind.module(), ""}
683+
{name, GenLSP.Enumerations.CompletionItemKind.module(), symbol.docs}
656684

657685
:variable ->
658686
{name, GenLSP.Enumerations.CompletionItemKind.variable(), ""}
@@ -666,6 +694,12 @@ defmodule NextLS do
666694
:keyword ->
667695
{name, GenLSP.Enumerations.CompletionItemKind.field(), ""}
668696

697+
:attribute ->
698+
{name, GenLSP.Enumerations.CompletionItemKind.property(), ""}
699+
700+
:sigil ->
701+
{name, GenLSP.Enumerations.CompletionItemKind.function(), ""}
702+
669703
_ ->
670704
{name, GenLSP.Enumerations.CompletionItemKind.text(), ""}
671705
end
@@ -838,6 +872,18 @@ defmodule NextLS do
838872

839873
parent = self()
840874

875+
elixir_bin_path =
876+
cond do
877+
lsp.assigns.init_opts.elixir_bin_path != nil ->
878+
lsp.assigns.init_opts.elixir_bin_path
879+
880+
lsp.assigns.init_opts.experimental.completions.enable ->
881+
NextLS.Runtime.BundledElixir.binpath()
882+
883+
true ->
884+
"elixir" |> System.find_executable() |> Path.dirname()
885+
end
886+
841887
for %{uri: uri, name: name} <- lsp.assigns.workspace_folders do
842888
token = Progress.token()
843889
Progress.start(lsp, token, "Initializing NextLS runtime for folder #{name}...")
@@ -859,6 +905,7 @@ defmodule NextLS do
859905
uri: uri,
860906
mix_env: lsp.assigns.init_opts.mix_env,
861907
mix_target: lsp.assigns.init_opts.mix_target,
908+
elixir_bin_path: elixir_bin_path,
862909
on_initialized: fn status ->
863910
if status == :ready do
864911
Progress.stop(lsp, token, "NextLS runtime for folder #{name} has initialized!")
@@ -870,7 +917,7 @@ defmodule NextLS do
870917
for {pid, _} <- entries, do: send(pid, msg)
871918
end)
872919

873-
send(parent, msg)
920+
Process.send(parent, msg, [])
874921
else
875922
Progress.stop(lsp, token)
876923

@@ -884,7 +931,7 @@ defmodule NextLS do
884931
)
885932
end
886933

887-
{:noreply, lsp}
934+
{:noreply, assign(lsp, elixir_bin_path: elixir_bin_path)}
888935
end
889936

890937
def handle_notification(%TextDocumentDidSave{}, %{assigns: %{ready: false}} = lsp) do
@@ -956,7 +1003,7 @@ defmodule NextLS do
9561003
},
9571004
lsp
9581005
) do
959-
dispatch(lsp.assigns.registry, :runtime_supervisors, fn entries ->
1006+
NextLS.Registry.dispatch(lsp.assigns.registry, :runtime_supervisors, fn entries ->
9601007
names = Enum.map(entries, fn {_, %{name: name}} -> name end)
9611008

9621009
for %{name: name, uri: uri} <- added, name not in names do
@@ -976,6 +1023,7 @@ defmodule NextLS do
9761023
runtime: [
9771024
task_supervisor: lsp.assigns.runtime_task_supervisor,
9781025
working_dir: working_dir,
1026+
elixir_bin_path: lsp.assigns.elixir_bin_path,
9791027
uri: uri,
9801028
mix_env: lsp.assigns.init_opts.mix_env,
9811029
mix_target: lsp.assigns.init_opts.mix_target,
@@ -1019,47 +1067,51 @@ defmodule NextLS do
10191067
lsp =
10201068
for %{type: type, uri: uri} <- changes, reduce: lsp do
10211069
lsp ->
1070+
file = URI.parse(uri).path
1071+
10221072
cond do
10231073
type == GenLSP.Enumerations.FileChangeType.created() ->
1024-
with {:ok, text} <- File.read(URI.parse(uri).path) do
1074+
with {:ok, text} <- File.read(file) do
10251075
put_in(lsp.assigns.documents[uri], String.split(text, "\n"))
10261076
else
10271077
_ -> lsp
10281078
end
10291079

10301080
type == GenLSP.Enumerations.FileChangeType.changed() ->
1031-
with {:ok, text} <- File.read(URI.parse(uri).path) do
1081+
with {:ok, text} <- File.read(file) do
10321082
put_in(lsp.assigns.documents[uri], String.split(text, "\n"))
10331083
else
10341084
_ -> lsp
10351085
end
10361086

10371087
type == GenLSP.Enumerations.FileChangeType.deleted() ->
1038-
dispatch(lsp.assigns.registry, :databases, fn entries ->
1039-
for {pid, _} <- entries do
1040-
file = URI.parse(uri).path
1041-
1042-
NextLS.DB.query(
1043-
pid,
1044-
~Q"""
1045-
DELETE FROM symbols
1046-
WHERE symbols.file = ?;
1047-
""",
1048-
[file]
1049-
)
1050-
1051-
NextLS.DB.query(
1052-
pid,
1053-
~Q"""
1054-
DELETE FROM 'references' AS refs
1055-
WHERE refs.file = ?;
1056-
""",
1057-
[file]
1058-
)
1059-
end
1060-
end)
1088+
if not File.exists?(file) do
1089+
dispatch(lsp.assigns.registry, :databases, fn entries ->
1090+
for {pid, _} <- entries do
1091+
NextLS.DB.query(
1092+
pid,
1093+
~Q"""
1094+
DELETE FROM symbols
1095+
WHERE symbols.file = ?;
1096+
""",
1097+
[file]
1098+
)
1099+
1100+
NextLS.DB.query(
1101+
pid,
1102+
~Q"""
1103+
DELETE FROM 'references' AS refs
1104+
WHERE refs.file = ?;
1105+
""",
1106+
[file]
1107+
)
1108+
end
1109+
end)
10611110

1062-
update_in(lsp.assigns.documents, &Map.drop(&1, [uri]))
1111+
update_in(lsp.assigns.documents, &Map.drop(&1, [uri]))
1112+
else
1113+
lsp
1114+
end
10631115
end
10641116
end
10651117

@@ -1136,25 +1188,28 @@ defmodule NextLS do
11361188
end
11371189

11381190
def handle_info({:runtime_ready, name, runtime_pid}, lsp) do
1139-
token = Progress.token()
1140-
Progress.start(lsp, token, "Compiling #{name}...")
1191+
case NextLS.Registry.dispatch(lsp.assigns.registry, :databases, fn entries ->
1192+
Enum.find(entries, fn {_, %{runtime: runtime}} -> runtime == name end)
1193+
end) do
1194+
{_, %{mode: mode}} ->
1195+
token = Progress.token()
1196+
Progress.start(lsp, token, "Compiling #{name}...")
11411197

1142-
{_, %{mode: mode}} =
1143-
dispatch(lsp.assigns.registry, :databases, fn entries ->
1144-
Enum.find(entries, fn {_, %{runtime: runtime}} -> runtime == name end)
1145-
end)
1198+
ref = make_ref()
1199+
Runtime.compile(runtime_pid, caller_ref: ref, force: mode == :reindex)
11461200

1147-
ref = make_ref()
1148-
Runtime.compile(runtime_pid, caller_ref: ref, force: mode == :reindex)
1201+
refresh_refs = Map.put(lsp.assigns.refresh_refs, ref, {token, "Compiled #{name}!"})
11491202

1150-
refresh_refs = Map.put(lsp.assigns.refresh_refs, ref, {token, "Compiled #{name}!"})
1203+
{:noreply, assign(lsp, ready: true, refresh_refs: refresh_refs)}
11511204

1152-
{:noreply, assign(lsp, ready: true, refresh_refs: refresh_refs)}
1205+
nil ->
1206+
{:noreply, assign(lsp, ready: true)}
1207+
end
11531208
end
11541209

11551210
def handle_info({:runtime_failed, name, status}, lsp) do
11561211
{pid, %{init_arg: init_arg}} =
1157-
dispatch(lsp.assigns.registry, :runtime_supervisors, fn entries ->
1212+
NextLS.Registry.dispatch(lsp.assigns.registry, :runtime_supervisors, fn entries ->
11581213
Enum.find(entries, fn {_pid, %{name: n}} -> n == name end)
11591214
end)
11601215

@@ -1186,6 +1241,7 @@ defmodule NextLS do
11861241
)
11871242

11881243
File.rm_rf!(Path.join(init_arg[:runtime][:working_dir], ".elixir-tools/_build"))
1244+
File.rm_rf!(Path.join(init_arg[:runtime][:working_dir], ".elixir-tools/_build2"))
11891245

11901246
case System.cmd("mix", ["deps.get"],
11911247
env: [{"MIX_ENV", "dev"}, {"MIX_BUILD_ROOT", ".elixir-tools/_build"}],
@@ -1267,6 +1323,9 @@ defmodule NextLS do
12671323

12681324
receive do
12691325
{^ref, result} -> result
1326+
after
1327+
1000 ->
1328+
:timeout
12701329
end
12711330
end
12721331

@@ -1441,6 +1500,7 @@ defmodule NextLS do
14411500

14421501
defstruct mix_target: "host",
14431502
mix_env: "dev",
1503+
elixir_bin_path: nil,
14441504
experimental: %NextLS.InitOpts.Experimental{},
14451505
extensions: %NextLS.InitOpts.Extensions{}
14461506

@@ -1450,6 +1510,8 @@ defmodule NextLS do
14501510
schema(__MODULE__, %{
14511511
optional(:mix_target) => str(),
14521512
optional(:mix_env) => str(),
1513+
optional(:mix_env) => str(),
1514+
optional(:elixir_bin_path) => str(),
14531515
optional(:experimental) =>
14541516
schema(NextLS.InitOpts.Experimental, %{
14551517
optional(:completions) =>

0 commit comments

Comments
 (0)