From 3f3c28c6b0fcac3e17fb9f876d9f9088d1a67359 Mon Sep 17 00:00:00 2001 From: Mitchell Hanberg Date: Sun, 23 Jul 2023 23:25:31 -0400 Subject: [PATCH] feat: adding/removing workspace folders --- lib/next_ls.ex | 62 +++++++++- lib/next_ls/runtime.ex | 10 +- lib/next_ls/runtime/supervisor.ex | 2 + test/next_ls_test.exs | 181 ++++++++++++++++++++++++------ 4 files changed, 213 insertions(+), 42 deletions(-) diff --git a/lib/next_ls.ex b/lib/next_ls.ex index b575a943..4f252eac 100644 --- a/lib/next_ls.ex +++ b/lib/next_ls.ex @@ -10,12 +10,14 @@ defmodule NextLS do alias GenLSP.Notifications.TextDocumentDidChange alias GenLSP.Notifications.TextDocumentDidOpen alias GenLSP.Notifications.TextDocumentDidSave + alias GenLSP.Notifications.WorkspaceDidChangeWorkspaceFolders alias GenLSP.Requests.Initialize alias GenLSP.Requests.Shutdown alias GenLSP.Requests.TextDocumentDefinition alias GenLSP.Requests.TextDocumentDocumentSymbol alias GenLSP.Requests.TextDocumentFormatting alias GenLSP.Requests.WorkspaceSymbol + alias GenLSP.Structures.DidChangeWorkspaceFoldersParams alias GenLSP.Structures.DidOpenTextDocumentParams alias GenLSP.Structures.InitializeParams alias GenLSP.Structures.InitializeResult @@ -28,6 +30,7 @@ defmodule NextLS do alias GenLSP.Structures.TextDocumentItem alias GenLSP.Structures.TextDocumentSyncOptions alias GenLSP.Structures.TextEdit + alias GenLSP.Structures.WorkspaceFoldersChangeEvent alias NextLS.Definition alias NextLS.DiagnosticCache alias NextLS.Progress @@ -288,7 +291,7 @@ defmodule NextLS do parent = self() working_dir = URI.parse(uri).path - {:ok, runtime} = + {:ok, _} = DynamicSupervisor.start_child( lsp.assigns.dynamic_supervisor, {NextLS.Runtime.Supervisor, @@ -312,8 +315,6 @@ defmodule NextLS do logger: lsp.assigns.logger ]} ) - - {name, %{uri: uri, runtime: runtime}} end {:noreply, lsp} @@ -379,6 +380,61 @@ defmodule NextLS do {:noreply, put_in(lsp.assigns.documents[uri], String.split(text, "\n"))} end + def handle_notification( + %WorkspaceDidChangeWorkspaceFolders{ + params: %DidChangeWorkspaceFoldersParams{event: %WorkspaceFoldersChangeEvent{added: added, removed: removed}} + }, + lsp + ) do + dispatch(lsp.assigns.registry, :runtime_supervisors, fn entries -> + names = Enum.map(entries, fn {_, %{name: name}} -> name end) + + for %{name: name, uri: uri} <- added, name not in names do + GenLSP.log(lsp, "[NextLS] Adding workspace folder #{name}") + token = token() + Progress.start(lsp, token, "Initializing NextLS runtime for folder #{name}...") + parent = self() + working_dir = URI.parse(uri).path + + # TODO: probably extract this to the Runtime module + {:ok, _} = + DynamicSupervisor.start_child( + lsp.assigns.dynamic_supervisor, + {NextLS.Runtime.Supervisor, + path: Path.join(working_dir, ".elixir-tools"), + name: name, + registry: lsp.assigns.registry, + runtime: [ + task_supervisor: lsp.assigns.runtime_task_supervisor, + working_dir: working_dir, + uri: uri, + on_initialized: fn status -> + if status == :ready do + Progress.stop(lsp, token, "NextLS runtime for folder #{name} has initialized!") + GenLSP.log(lsp, "[NextLS] Runtime for folder #{name} is ready...") + send(parent, {:runtime_ready, name, self()}) + else + Progress.stop(lsp, token) + GenLSP.error(lsp, "[NextLS] Runtime for folder #{name} failed to initialize") + end + end, + logger: lsp.assigns.logger + ]} + ) + end + + names = Enum.map(removed, & &1.name) + + for {pid, %{name: name}} <- entries, name in names do + GenLSP.log(lsp, "[NextLS] Removing workspace folder #{name}") + # TODO: probably extract this to the Runtime module + DynamicSupervisor.terminate_child(lsp.assigns.dynamic_supervisor, pid) + end + end) + + {:noreply, lsp} + end + def handle_notification(%Exit{}, lsp) do System.halt(lsp.assigns.exit_code) diff --git a/lib/next_ls/runtime.ex b/lib/next_ls/runtime.ex index b4ec38ab..444b6917 100644 --- a/lib/next_ls/runtime.ex +++ b/lib/next_ls/runtime.ex @@ -101,8 +101,14 @@ defmodule NextLS.Runtime do ref = Process.monitor(me) receive do - {:DOWN, ^ref, :process, ^me, _reason} -> - NextLS.Logger.error(logger, "[NextLS] The runtime for #{name} has crashed") + {:DOWN, ^ref, :process, ^me, reason} -> + case reason do + :shutdown -> + NextLS.Logger.log(logger, "The runtime for #{name} has successfully shutdown.") + + reason -> + NextLS.Logger.error(logger, "The runtime for #{name} has crashed with reason: #{reason}.") + end end end) diff --git a/lib/next_ls/runtime/supervisor.ex b/lib/next_ls/runtime/supervisor.ex index f38ee6c7..e2a51025 100644 --- a/lib/next_ls/runtime/supervisor.ex +++ b/lib/next_ls/runtime/supervisor.ex @@ -18,6 +18,8 @@ defmodule NextLS.Runtime.Supervisor do symbol_table_name = :"symbol-table-#{name}" sidecar_name = :"sidecar-#{name}" + Registry.register(registry, :runtime_supervisors, %{name: name}) + children = [ {NextLS.SymbolTable, workspace: name, path: hidden_folder, registry: registry, name: symbol_table_name}, {NextLS.Runtime.Sidecar, name: sidecar_name, symbol_table: symbol_table_name}, diff --git a/test/next_ls_test.exs b/test/next_ls_test.exs index d890bcb5..708f694e 100644 --- a/test/next_ls_test.exs +++ b/test/next_ls_test.exs @@ -6,15 +6,16 @@ defmodule NextLSTest do @moduletag :tmp_dir - setup %{tmp_dir: tmp_dir} do - File.mkdir_p!(Path.join(tmp_dir, "lib")) - File.write!(Path.join(tmp_dir, "mix.exs"), mix_exs()) - [cwd: tmp_dir] - end - describe "one" do + @describetag root_paths: ["my_proj"] + setup %{tmp_dir: tmp_dir} do + File.mkdir_p!(Path.join(tmp_dir, "my_proj/lib")) + File.write!(Path.join(tmp_dir, "my_proj/mix.exs"), mix_exs()) + [cwd: tmp_dir] + end + setup %{tmp_dir: tmp_dir} do - File.write!(Path.join(tmp_dir, "lib/bar.ex"), """ + File.write!(Path.join(tmp_dir, "my_proj/lib/bar.ex"), """ defmodule Bar do defstruct [:foo] @@ -23,7 +24,7 @@ defmodule NextLSTest do end """) - File.write!(Path.join(tmp_dir, "lib/code_action.ex"), """ + File.write!(Path.join(tmp_dir, "my_proj/lib/code_action.ex"), """ defmodule Foo.CodeAction do # some comment @@ -35,12 +36,12 @@ defmodule NextLSTest do end """) - File.write!(Path.join(tmp_dir, "lib/foo.ex"), """ + File.write!(Path.join(tmp_dir, "my_proj/lib/foo.ex"), """ defmodule Foo do end """) - File.write!(Path.join(tmp_dir, "lib/project.ex"), """ + File.write!(Path.join(tmp_dir, "my_proj/lib/project.ex"), """ defmodule Project do def hello do :world @@ -156,7 +157,7 @@ defmodule NextLSTest do to_string(%URI{ host: "", scheme: "file", - path: Path.join([cwd, "lib", file]) + path: Path.join([cwd, "my_proj/lib", file]) }) char = if Version.match?(System.version(), ">= 1.15.0"), do: 10, else: 0 @@ -192,7 +193,7 @@ defmodule NextLSTest do jsonrpc: "2.0", params: %{ textDocument: %{ - uri: "file://#{cwd}/lib/foo/bar.ex", + uri: "file://#{cwd}/my_proj/lib/foo/bar.ex", languageId: "elixir", version: 1, text: """ @@ -214,7 +215,7 @@ defmodule NextLSTest do jsonrpc: "2.0", params: %{ textDocument: %{ - uri: "file://#{cwd}/lib/foo/bar.ex" + uri: "file://#{cwd}/my_proj/lib/foo/bar.ex" }, options: %{ insertSpaces: true, @@ -233,7 +234,7 @@ defmodule NextLSTest do jsonrpc: "2.0", params: %{ textDocument: %{ - uri: "file://#{cwd}/lib/foo/bar.ex" + uri: "file://#{cwd}/my_proj/lib/foo/bar.ex" }, options: %{ insertSpaces: true, @@ -271,7 +272,7 @@ defmodule NextLSTest do jsonrpc: "2.0", params: %{ textDocument: %{ - uri: "file://#{cwd}/lib/foo/bar.ex", + uri: "file://#{cwd}/my_proj/lib/foo/bar.ex", languageId: "elixir", version: 1, text: """ @@ -294,7 +295,7 @@ defmodule NextLSTest do jsonrpc: "2.0", params: %{ textDocument: %{ - uri: "file://#{cwd}/lib/foo/bar.ex" + uri: "file://#{cwd}/my_proj/lib/foo/bar.ex" }, options: %{ insertSpaces: true, @@ -341,7 +342,7 @@ defmodule NextLSTest do "character" => 0 } }, - "uri" => "file://#{cwd}/lib/bar.ex" + "uri" => "file://#{cwd}/my_proj/lib/bar.ex" }, "name" => "def foo" } in symbols @@ -359,7 +360,7 @@ defmodule NextLSTest do "character" => 0 } }, - "uri" => "file://#{cwd}/lib/bar.ex" + "uri" => "file://#{cwd}/my_proj/lib/bar.ex" }, "name" => "defmodule Bar" } in symbols @@ -377,7 +378,7 @@ defmodule NextLSTest do "character" => 0 } }, - "uri" => "file://#{cwd}/lib/bar.ex" + "uri" => "file://#{cwd}/my_proj/lib/bar.ex" }, "name" => "%Bar{}" } in symbols @@ -395,7 +396,7 @@ defmodule NextLSTest do "character" => 0 } }, - "uri" => "file://#{cwd}/lib/code_action.ex" + "uri" => "file://#{cwd}/my_proj/lib/code_action.ex" }, "name" => "defmodule Foo.CodeAction.NestedMod" } in symbols @@ -437,7 +438,7 @@ defmodule NextLSTest do "character" => 0 } }, - "uri" => "file://#{cwd}/lib/bar.ex" + "uri" => "file://#{cwd}/my_proj/lib/bar.ex" }, "name" => "def foo" }, @@ -454,7 +455,7 @@ defmodule NextLSTest do "character" => 0 } }, - "uri" => "file://#{cwd}/lib/code_action.ex" + "uri" => "file://#{cwd}/my_proj/lib/code_action.ex" }, "name" => "def foo" } @@ -463,8 +464,15 @@ defmodule NextLSTest do end describe "function go to definition" do + @describetag root_paths: ["my_proj"] + setup %{tmp_dir: tmp_dir} do + File.mkdir_p!(Path.join(tmp_dir, "my_proj/lib")) + File.write!(Path.join(tmp_dir, "my_proj/mix.exs"), mix_exs()) + [cwd: tmp_dir] + end + setup %{cwd: cwd} do - remote = Path.join(cwd, "lib/remote.ex") + remote = Path.join(cwd, "my_proj/lib/remote.ex") File.write!(remote, """ defmodule Remote do @@ -474,7 +482,7 @@ defmodule NextLSTest do end """) - imported = Path.join(cwd, "lib/imported.ex") + imported = Path.join(cwd, "my_proj/lib/imported.ex") File.write!(imported, """ defmodule Imported do @@ -484,7 +492,7 @@ defmodule NextLSTest do end """) - bar = Path.join(cwd, "lib/bar.ex") + bar = Path.join(cwd, "my_proj/lib/bar.ex") File.write!(bar, """ defmodule Foo do @@ -626,8 +634,15 @@ defmodule NextLSTest do end describe "macro go to definition" do + @describetag root_paths: ["my_proj"] + setup %{tmp_dir: tmp_dir} do + File.mkdir_p!(Path.join(tmp_dir, "my_proj/lib")) + File.write!(Path.join(tmp_dir, "my_proj/mix.exs"), mix_exs()) + [cwd: tmp_dir] + end + setup %{cwd: cwd} do - remote = Path.join(cwd, "lib/remote.ex") + remote = Path.join(cwd, "my_proj/lib/remote.ex") File.write!(remote, """ defmodule Remote do @@ -639,7 +654,7 @@ defmodule NextLSTest do end """) - imported = Path.join(cwd, "lib/imported.ex") + imported = Path.join(cwd, "my_proj/lib/imported.ex") File.write!(imported, """ defmodule Imported do @@ -651,7 +666,7 @@ defmodule NextLSTest do end """) - bar = Path.join(cwd, "lib/bar.ex") + bar = Path.join(cwd, "my_proj/lib/bar.ex") File.write!(bar, """ defmodule Foo do @@ -797,8 +812,15 @@ defmodule NextLSTest do end describe "module go to definition" do + @describetag root_paths: ["my_proj"] + setup %{tmp_dir: tmp_dir} do + File.mkdir_p!(Path.join(tmp_dir, "my_proj/lib")) + File.write!(Path.join(tmp_dir, "my_proj/mix.exs"), mix_exs()) + [cwd: tmp_dir] + end + setup %{cwd: cwd} do - peace = Path.join(cwd, "lib/peace.ex") + peace = Path.join(cwd, "my_proj/lib/peace.ex") File.write!(peace, """ defmodule MyApp.Peace do @@ -808,7 +830,7 @@ defmodule NextLSTest do end """) - bar = Path.join(cwd, "lib/bar.ex") + bar = Path.join(cwd, "my_proj/lib/bar.ex") File.write!(bar, """ defmodule Bar do @@ -861,8 +883,96 @@ defmodule NextLSTest do end end - defp with_lsp(%{tmp_dir: tmp_dir}) do - root_path = Path.absname(tmp_dir) + describe "workspaces" do + setup %{tmp_dir: tmp_dir} do + [cwd: tmp_dir] + end + + setup %{cwd: cwd} do + File.mkdir_p!(Path.join(cwd, "proj_one/lib")) + File.write!(Path.join(cwd, "proj_one/mix.exs"), mix_exs()) + peace = Path.join(cwd, "proj_one/lib/peace.ex") + + File.write!(peace, """ + defmodule MyApp.Peace do + def and_love() do + "✌️" + end + end + """) + + File.mkdir_p!(Path.join(cwd, "proj_two/lib")) + File.write!(Path.join(cwd, "proj_two/mix.exs"), mix_exs()) + bar = Path.join(cwd, "proj_two/lib/bar.ex") + + File.write!(bar, """ + defmodule Bar do + def run() do + MyApp.Peace.and_love() + end + end + """) + + [bar: bar, peace: peace] + end + + setup :with_lsp + + @tag root_paths: ["proj_one"] + test "starts a new runtime when you add a workspace folder", %{client: client, cwd: cwd} do + assert :ok == notify(client, %{method: "initialized", jsonrpc: "2.0", params: %{}}) + assert_notification "window/logMessage", %{"message" => "[NextLS] Runtime for folder proj_one is ready..."} + assert_notification "window/logMessage", %{"message" => "[NextLS] Compiled!"} + + notify(client, %{ + method: "workspace/didChangeWorkspaceFolders", + jsonrpc: "2.0", + params: %{ + event: %{ + added: [ + %{name: "proj_two", uri: "file://#{Path.join(cwd, "proj_two")}"} + ], + removed: [] + } + } + }) + + assert_notification "window/logMessage", %{"message" => "[NextLS] Runtime for folder proj_two is ready..."} + assert_notification "window/logMessage", %{"message" => "[NextLS] Compiled!"} + end + + @tag root_paths: ["proj_one", "proj_two"] + test "stops the runtime when you remove a workspace folder", %{client: client, cwd: cwd} do + assert :ok == notify(client, %{method: "initialized", jsonrpc: "2.0", params: %{}}) + assert_notification "window/logMessage", %{"message" => "[NextLS] Runtime for folder proj_one is ready..."} + assert_notification "window/logMessage", %{"message" => "[NextLS] Runtime for folder proj_two is ready..."} + assert_notification "window/logMessage", %{"message" => "[NextLS] Compiled!"} + assert_notification "window/logMessage", %{"message" => "[NextLS] Compiled!"} + + notify(client, %{ + method: "workspace/didChangeWorkspaceFolders", + jsonrpc: "2.0", + params: %{ + event: %{ + added: [], + removed: [ + %{name: "proj_two", uri: "file://#{Path.join(cwd, "proj_two")}"} + ] + } + } + }) + + assert_notification "window/logMessage", %{ + "message" => "[NextLS] The runtime for proj_two has successfully shutdown." + } + end + end + + defp with_lsp(%{tmp_dir: tmp_dir} = context) do + root_paths = + for path <- context[:root_paths] || [""] do + Path.absname(Path.join(tmp_dir, path)) + end tvisor = start_supervised!(Supervisor.child_spec(Task.Supervisor, id: :one)) r_tvisor = start_supervised!(Supervisor.child_spec(Task.Supervisor, id: :two)) @@ -896,14 +1006,11 @@ defmodule NextLSTest do workspaceFolders: true } }, - workspaceFolders: [ - %{uri: "file://#{root_path}", name: "my_proj"} - ], - rootUri: "file://#{root_path}" + workspaceFolders: for(path <- root_paths, do: %{uri: "file://#{path}", name: Path.basename(path)}) } }) - [server: server, client: client, cwd: root_path] + [server: server, client: client] end defp uri(path) when is_binary(path) do