Skip to content

Commit 639493c

Browse files
authored
fix: use registry for runtime messaging (#121)
This uses a registry to track the runtimes, so that if one crashes and is restarted, it will register itself with the registry and the LSP process doesn't need to track them itself. This also streamlines the logic for waiting for the the runtimes to be ready.
1 parent e9c317e commit 639493c

File tree

6 files changed

+179
-158
lines changed

6 files changed

+179
-158
lines changed

lib/next_ls.ex

Lines changed: 128 additions & 132 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ defmodule NextLS do
4343
:runtime_task_supervisor,
4444
:dynamic_supervisor,
4545
:extensions,
46-
:extension_registry,
46+
:registry,
4747
:symbol_table
4848
])
4949

@@ -55,7 +55,8 @@ defmodule NextLS do
5555
task_supervisor = Keyword.fetch!(args, :task_supervisor)
5656
runtime_task_supervisor = Keyword.fetch!(args, :runtime_task_supervisor)
5757
dynamic_supervisor = Keyword.fetch!(args, :dynamic_supervisor)
58-
extension_registry = Keyword.fetch!(args, :extension_registry)
58+
59+
registry = Keyword.fetch!(args, :registry)
5960
extensions = Keyword.get(args, :extensions, [NextLS.ElixirExtension])
6061
cache = Keyword.fetch!(args, :cache)
6162
symbol_table = Keyword.fetch!(args, :symbol_table)
@@ -72,9 +73,8 @@ defmodule NextLS do
7273
task_supervisor: task_supervisor,
7374
runtime_task_supervisor: runtime_task_supervisor,
7475
dynamic_supervisor: dynamic_supervisor,
75-
extension_registry: extension_registry,
76+
registry: registry,
7677
extensions: extensions,
77-
runtime_tasks: nil,
7878
ready: false,
7979
client_capabilities: nil
8080
)}
@@ -208,39 +208,44 @@ defmodule NextLS do
208208
def handle_request(%TextDocumentFormatting{params: %{text_document: %{uri: uri}}}, lsp) do
209209
document = lsp.assigns.documents[uri]
210210

211-
{_, %{runtime: runtime}} =
212-
Enum.find(lsp.assigns.runtimes, fn {_name, %{uri: wuri}} -> String.starts_with?(uri, wuri) end)
213-
214-
with {:ok, {formatter, _}} <- Runtime.call(runtime, {Mix.Tasks.Format, :formatter_for_file, [".formatter.exs"]}),
215-
{:ok, response} when is_binary(response) or is_list(response) <-
216-
Runtime.call(runtime, {Kernel, :apply, [formatter, [Enum.join(document, "\n")]]}) do
217-
{:reply,
218-
[
219-
%TextEdit{
220-
new_text: IO.iodata_to_binary(response),
221-
range: %Range{
222-
start: %Position{line: 0, character: 0},
223-
end: %Position{
224-
line: length(document),
225-
character: document |> List.last() |> String.length() |> Kernel.-(1) |> max(0)
226-
}
227-
}
228-
}
229-
], lsp}
230-
else
231-
{:error, :not_ready} ->
232-
GenLSP.notify(lsp, %GenLSP.Notifications.WindowShowMessage{
233-
params: %GenLSP.Structures.ShowMessageParams{
234-
type: GenLSP.Enumerations.MessageType.info(),
235-
message: "The NextLS runtime is still initializing!"
236-
}
237-
})
238-
239-
{:reply, nil, lsp}
211+
[resp] =
212+
dispatch(lsp.assigns.registry, :runtimes, fn entries ->
213+
for {runtime, %{uri: wuri}} <- entries, String.starts_with?(uri, wuri) do
214+
with {:ok, {formatter, _}} <-
215+
Runtime.call(runtime, {Mix.Tasks.Format, :formatter_for_file, [".formatter.exs"]}),
216+
{:ok, response} when is_binary(response) or is_list(response) <-
217+
Runtime.call(runtime, {Kernel, :apply, [formatter, [Enum.join(document, "\n")]]}) do
218+
{:reply,
219+
[
220+
%TextEdit{
221+
new_text: IO.iodata_to_binary(response),
222+
range: %Range{
223+
start: %Position{line: 0, character: 0},
224+
end: %Position{
225+
line: length(document),
226+
character: document |> List.last() |> String.length() |> Kernel.-(1) |> max(0)
227+
}
228+
}
229+
}
230+
], lsp}
231+
else
232+
{:error, :not_ready} ->
233+
GenLSP.notify(lsp, %GenLSP.Notifications.WindowShowMessage{
234+
params: %GenLSP.Structures.ShowMessageParams{
235+
type: GenLSP.Enumerations.MessageType.info(),
236+
message: "The NextLS runtime is still initializing!"
237+
}
238+
})
239+
240+
{:reply, nil, lsp}
241+
242+
_ ->
243+
{:reply, nil, lsp}
244+
end
245+
end
246+
end)
240247

241-
_ ->
242-
{:reply, nil, lsp}
243-
end
248+
resp
244249
end
245250

246251
def handle_request(%Shutdown{}, lsp) do
@@ -267,87 +272,79 @@ defmodule NextLS do
267272
{:ok, _} =
268273
DynamicSupervisor.start_child(
269274
lsp.assigns.dynamic_supervisor,
270-
{extension, cache: lsp.assigns.cache, registry: lsp.assigns.extension_registry, publisher: self()}
275+
{extension, cache: lsp.assigns.cache, registry: lsp.assigns.registry, publisher: self()}
271276
)
272277
end
273278

274-
GenLSP.log(lsp, "[NextLS] Booting runtime...")
275-
276-
runtimes =
277-
for %{uri: uri, name: name} <- lsp.assigns.workspace_folders do
278-
token = token()
279-
progress_start(lsp, token, "Initializing NextLS runtime for folder #{name}...")
279+
GenLSP.log(lsp, "[NextLS] Booting runtimes...")
280280

281-
{:ok, runtime} =
282-
DynamicSupervisor.start_child(
283-
lsp.assigns.dynamic_supervisor,
284-
{NextLS.Runtime,
285-
task_supervisor: lsp.assigns.runtime_task_supervisor,
286-
extension_registry: lsp.assigns.extension_registry,
287-
working_dir: URI.parse(uri).path,
288-
parent: self(),
289-
logger: lsp.assigns.logger}
290-
)
281+
for %{uri: uri, name: name} <- lsp.assigns.workspace_folders do
282+
token = token()
283+
progress_start(lsp, token, "Initializing NextLS runtime for folder #{name}...")
284+
parent = self()
291285

292-
Process.monitor(runtime)
293-
294-
{name,
295-
%{uri: uri, runtime: runtime, refresh_ref: {token, "NextLS runtime for folder #{name} has initialized!"}}}
296-
end
297-
298-
lsp = assign(lsp, runtimes: Map.new(runtimes))
299-
300-
tasks =
301-
for {name, workspace} <- runtimes do
302-
Task.Supervisor.async_nolink(lsp.assigns.task_supervisor, fn ->
303-
with false <- wait_until(fn -> NextLS.Runtime.ready?(workspace.runtime) end) do
304-
GenLSP.error(lsp, "[NextLS] Failed to start runtime for folder #{name}")
305-
raise "Failed to boot runtime"
306-
end
286+
{:ok, runtime} =
287+
DynamicSupervisor.start_child(
288+
lsp.assigns.dynamic_supervisor,
289+
{NextLS.Runtime,
290+
name: name,
291+
task_supervisor: lsp.assigns.runtime_task_supervisor,
292+
registry: lsp.assigns.registry,
293+
working_dir: URI.parse(uri).path,
294+
uri: uri,
295+
parent: self(),
296+
on_initialized: fn status ->
297+
if status == :ready do
298+
progress_end(lsp, token, "NextLS runtime for folder #{name} has initialized!")
299+
GenLSP.log(lsp, "[NextLS] Runtime for folder #{name} is ready...")
300+
send(parent, {:runtime_ready, name, self()})
301+
else
302+
progress_end(lsp, token)
303+
GenLSP.error(lsp, "[NextLS] Runtime for folder #{name} failed to initialize")
304+
end
305+
end,
306+
logger: lsp.assigns.logger}
307+
)
307308

308-
GenLSP.log(lsp, "[NextLS] Runtime for folder #{name} is ready...")
309+
ref = Process.monitor(runtime)
309310

310-
{name, :ready}
311-
end)
312-
end
311+
Process.put(ref, name)
313312

314-
refresh_refs =
315-
tasks |> Enum.zip_with(runtimes, fn task, {_name, runtime} -> {task.ref, runtime.refresh_ref} end) |> Map.new()
313+
{name, %{uri: uri, runtime: runtime}}
314+
end
316315

317-
{:noreply,
318-
assign(lsp,
319-
refresh_refs: Map.merge(lsp.assigns.refresh_refs, refresh_refs),
320-
runtime_tasks: tasks
321-
)}
316+
{:noreply, lsp}
322317
end
323318

324319
def handle_notification(%TextDocumentDidSave{}, %{assigns: %{ready: false}} = lsp) do
325320
{:noreply, lsp}
326321
end
327322

323+
# TODO: add some test cases for saving files in multiple workspaces
328324
def handle_notification(
329325
%TextDocumentDidSave{
330326
params: %GenLSP.Structures.DidSaveTextDocumentParams{text: text, text_document: %{uri: uri}}
331327
},
332328
%{assigns: %{ready: true}} = lsp
333329
) do
334-
for task <- Task.Supervisor.children(lsp.assigns.task_supervisor),
335-
task not in for(t <- lsp.assigns.runtime_tasks, do: t.pid) do
330+
for task <- Task.Supervisor.children(lsp.assigns.task_supervisor) do
336331
Process.exit(task, :kill)
337332
end
338333

339-
token = token()
334+
refresh_refs =
335+
dispatch(lsp.assigns.registry, :runtimes, fn entries ->
336+
for {pid, %{name: name, uri: wuri}} <- entries, String.starts_with?(uri, wuri), into: %{} do
337+
token = token()
338+
progress_start(lsp, token, "Compiling...")
340339

341-
progress_start(lsp, token, "Compiling...")
342-
runtimes = Enum.to_list(lsp.assigns.runtimes)
340+
task =
341+
Task.Supervisor.async_nolink(lsp.assigns.task_supervisor, fn ->
342+
{name, Runtime.compile(pid)}
343+
end)
343344

344-
tasks =
345-
for {name, r} <- runtimes do
346-
Task.Supervisor.async_nolink(lsp.assigns.task_supervisor, fn -> {name, Runtime.compile(r.runtime)} end)
347-
end
348-
349-
refresh_refs =
350-
tasks |> Enum.zip_with(runtimes, fn task, {_name, runtime} -> {task.ref, runtime.refresh_ref} end) |> Map.new()
345+
{task.ref, {token, "Compiled!"}}
346+
end
347+
end)
351348

352349
{:noreply,
353350
lsp
@@ -363,8 +360,7 @@ defmodule NextLS do
363360
%TextDocumentDidChange{params: %{text_document: %{uri: uri}, content_changes: [%{text: text}]}},
364361
lsp
365362
) do
366-
for task <- Task.Supervisor.children(lsp.assigns.task_supervisor),
367-
task not in for(t <- lsp.assigns.runtime_tasks, do: t.pid) do
363+
for task <- Task.Supervisor.children(lsp.assigns.task_supervisor) do
368364
Process.exit(task, :kill)
369365
end
370366

@@ -420,30 +416,27 @@ defmodule NextLS do
420416
{:noreply, lsp}
421417
end
422418

423-
def handle_info({ref, resp}, %{assigns: %{refresh_refs: refs}} = lsp) when is_map_key(refs, ref) do
424-
Process.demonitor(ref, [:flush])
425-
{{token, msg}, refs} = Map.pop(refs, ref)
419+
def handle_info({:runtime_ready, name, runtime_pid}, lsp) do
420+
token = token()
421+
progress_start(lsp, token, "Compiling...")
426422

427-
progress_end(lsp, token, msg)
423+
task =
424+
Task.Supervisor.async_nolink(lsp.assigns.task_supervisor, fn ->
425+
{name, Runtime.compile(runtime_pid)}
426+
end)
428427

429-
lsp =
430-
case resp do
431-
{name, :ready} ->
432-
token = token()
433-
progress_start(lsp, token, "Compiling...")
428+
refresh_refs = Map.put(lsp.assigns.refresh_refs, task.ref, {token, "Compiled!"})
434429

435-
task =
436-
Task.Supervisor.async_nolink(lsp.assigns.task_supervisor, fn ->
437-
{name, Runtime.compile(lsp.assigns.runtimes[name].runtime)}
438-
end)
430+
{:noreply, assign(lsp, ready: true, refresh_refs: refresh_refs)}
431+
end
439432

440-
assign(lsp, ready: true, refresh_refs: Map.put(refs, task.ref, {token, "Compiled!"}))
433+
def handle_info({ref, _resp}, %{assigns: %{refresh_refs: refs}} = lsp) when is_map_key(refs, ref) do
434+
Process.demonitor(ref, [:flush])
435+
{{token, msg}, refs} = Map.pop(refs, ref)
441436

442-
_ ->
443-
assign(lsp, refresh_refs: refs)
444-
end
437+
progress_end(lsp, token, msg)
445438

446-
{:noreply, lsp}
439+
{:noreply, assign(lsp, refresh_refs: refs)}
447440
end
448441

449442
def handle_info({:DOWN, ref, :process, _pid, _reason}, %{assigns: %{refresh_refs: refs}} = lsp)
@@ -455,35 +448,20 @@ defmodule NextLS do
455448
{:noreply, assign(lsp, refresh_refs: refs)}
456449
end
457450

458-
def handle_info({:DOWN, _ref, :process, runtime, _reason}, %{assigns: %{runtimes: runtimes}} = lsp) do
459-
{name, _} = Enum.find(runtimes, fn {_name, %{runtime: r}} -> r == runtime end)
451+
def handle_info({:DOWN, ref, :process, _runtime, _reason}, lsp) do
452+
name = Process.get(ref)
453+
Process.delete(ref)
454+
460455
GenLSP.error(lsp, "[NextLS] The runtime for #{name} has crashed")
461456

462-
{:noreply, assign(lsp, runtimes: Map.drop(runtimes, name))}
457+
{:noreply, lsp}
463458
end
464459

465460
def handle_info(message, lsp) do
466-
GenLSP.log(lsp, "[NextLS] Unhanded message: #{inspect(message)}")
461+
GenLSP.log(lsp, "[NextLS] Unhandled message: #{inspect(message)}")
467462
{:noreply, lsp}
468463
end
469464

470-
defp wait_until(cb) do
471-
wait_until(120, cb)
472-
end
473-
474-
defp wait_until(0, _cb) do
475-
false
476-
end
477-
478-
defp wait_until(n, cb) do
479-
if cb.() do
480-
true
481-
else
482-
Process.sleep(1000)
483-
wait_until(n - 1, cb)
484-
end
485-
end
486-
487465
defp progress_start(lsp, token, msg) do
488466
GenLSP.notify(lsp, %GenLSP.Notifications.DollarProgress{
489467
params: %GenLSP.Structures.ProgressParams{
@@ -527,4 +505,22 @@ defmodule NextLS do
527505

528506
defp elixir_kind_to_lsp_kind(kind) when kind in [:def, :defp, :defmacro, :defmacrop],
529507
do: GenLSP.Enumerations.SymbolKind.function()
508+
509+
# NOTE: this is only possible because the registry is not partitioned
510+
# if it is partitioned, then the callback is called multiple times
511+
# and this method of extracting the result doesn't really make sense
512+
defp dispatch(registry, key, callback) do
513+
ref = make_ref()
514+
me = self()
515+
516+
Registry.dispatch(registry, key, fn entries ->
517+
result = callback.(entries)
518+
519+
send(me, {ref, result})
520+
end)
521+
522+
receive do
523+
{^ref, result} -> result
524+
end
525+
end
530526
end

lib/next_ls/extensions/elixir_extension.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ defmodule NextLS.ElixirExtension do
1818
registry = Keyword.fetch!(args, :registry)
1919
publisher = Keyword.fetch!(args, :publisher)
2020

21-
Registry.register(registry, :extension, :elixir)
21+
Registry.register(registry, :extensions, :elixir)
2222

2323
{:ok, %{cache: cache, registry: registry, publisher: publisher}}
2424
end

lib/next_ls/lsp_supervisor.ex

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,14 +65,14 @@ defmodule NextLS.LSPSupervisor do
6565
{GenLSP.Buffer, buffer_opts},
6666
{NextLS.DiagnosticCache, name: :diagnostic_cache},
6767
{NextLS.SymbolTable, name: :symbol_table, path: hidden_folder},
68-
{Registry, name: NextLS.ExtensionRegistry, keys: :duplicate},
68+
{Registry, name: NextLS.Registry, keys: :duplicate},
6969
{NextLS,
7070
cache: :diagnostic_cache,
7171
symbol_table: :symbol_table,
7272
task_supervisor: NextLS.TaskSupervisor,
7373
runtime_task_supervisor: :runtime_task_supervisor,
7474
dynamic_supervisor: NextLS.DynamicSupervisor,
75-
extension_registry: NextLS.ExtensionRegistry}
75+
registry: NextLS.Registry}
7676
]
7777

7878
Supervisor.init(children, strategy: :one_for_one)

0 commit comments

Comments
 (0)