From 6b5ffafda9c754f59548760384387dbe66cf8e3b Mon Sep 17 00:00:00 2001 From: Mitchell Hanberg Date: Fri, 21 Jul 2023 00:27:06 -0400 Subject: [PATCH] feat: workspace folders on startup (#117) --- lib/next_ls.ex | 136 +++++++++++++++++++++++++----------------- test/next_ls_test.exs | 64 +++++++++++--------- 2 files changed, 117 insertions(+), 83 deletions(-) diff --git a/lib/next_ls.ex b/lib/next_ls.ex index fd84aa88..9eec9b89 100644 --- a/lib/next_ls.ex +++ b/lib/next_ls.ex @@ -87,13 +87,26 @@ defmodule NextLS do dynamic_supervisor: dynamic_supervisor, extension_registry: extension_registry, extensions: extensions, - runtime_task: nil, - ready: false + runtime_tasks: nil, + ready: false, + client_capabilities: nil )} end @impl true - def handle_request(%Initialize{params: %InitializeParams{root_uri: root_uri}}, lsp) do + def handle_request( + %Initialize{ + params: %InitializeParams{root_uri: root_uri, workspace_folders: workspace_folders, capabilities: caps} + }, + lsp + ) do + workspace_folders = + if caps.workspace.workspace_folders do + workspace_folders + else + %{name: Path.basename(root_uri), uri: root_uri} + end + {:reply, %InitializeResult{ capabilities: %ServerCapabilities{ @@ -105,10 +118,16 @@ defmodule NextLS do document_formatting_provider: true, workspace_symbol_provider: true, document_symbol_provider: true, - definition_provider: true + definition_provider: true, + workspace: %{ + workspace_folders: %GenLSP.Structures.WorkspaceFoldersServerCapabilities{ + supported: true, + change_notifications: true + } + } }, - server_info: %{name: "NextLS"} - }, assign(lsp, root_uri: root_uri)} + server_info: %{name: "Next LS"} + }, assign(lsp, root_uri: root_uri, workspace_folders: workspace_folders, client_capabilities: caps)} end def handle_request(%TextDocumentDefinition{params: %{text_document: %{uri: uri}, position: position}}, lsp) do @@ -201,7 +220,9 @@ defmodule NextLS do def handle_request(%TextDocumentFormatting{params: %{text_document: %{uri: uri}}}, lsp) do document = lsp.assigns.documents[uri] - runtime = lsp.assigns.runtime + + {_, %{runtime: runtime}} = + lsp.assigns.runtimes |> Enum.find(fn {_name, %{uri: wuri}} -> String.starts_with?(uri, wuri) end) with {:ok, {formatter, _}} <- Runtime.call(runtime, {Mix.Tasks.Format, :formatter_for_file, [".formatter.exs"]}), {:ok, response} when is_binary(response) or is_list(response) <- @@ -255,8 +276,6 @@ defmodule NextLS do def handle_notification(%Initialized{}, lsp) do GenLSP.log(lsp, "[NextLS] NextLS v#{version()} has initialized!") - working_dir = URI.parse(lsp.assigns.root_uri).path - for extension <- lsp.assigns.extensions do {:ok, _} = DynamicSupervisor.start_child( @@ -267,44 +286,51 @@ defmodule NextLS do GenLSP.log(lsp, "[NextLS] Booting runtime...") - token = token() - - progress_start(lsp, token, "Initializing NextLS runtime...") - - {:ok, runtime} = - DynamicSupervisor.start_child( - lsp.assigns.dynamic_supervisor, - {NextLS.Runtime, - task_supervisor: lsp.assigns.runtime_task_supervisor, - extension_registry: lsp.assigns.extension_registry, - working_dir: working_dir, - parent: self(), - logger: lsp.assigns.logger} - ) + runtimes = + for %{uri: uri, name: name} <- lsp.assigns.workspace_folders do + token = token() + progress_start(lsp, token, "Initializing NextLS runtime for folder #{name}...") + + {:ok, runtime} = + DynamicSupervisor.start_child( + lsp.assigns.dynamic_supervisor, + {NextLS.Runtime, + task_supervisor: lsp.assigns.runtime_task_supervisor, + extension_registry: lsp.assigns.extension_registry, + working_dir: URI.parse(uri).path, + parent: self(), + logger: lsp.assigns.logger} + ) + + Process.monitor(runtime) + + {name, + %{uri: uri, runtime: runtime, refresh_ref: {token, "NextLS runtime for folder #{name} has initialized!"}}} + end - Process.monitor(runtime) + lsp = assign(lsp, runtimes: Map.new(runtimes)) - lsp = assign(lsp, runtime: runtime) + tasks = + for {name, workspace} <- runtimes do + Task.Supervisor.async_nolink(lsp.assigns.task_supervisor, fn -> + with false <- wait_until(fn -> NextLS.Runtime.ready?(workspace.runtime) end) do + GenLSP.error(lsp, "[NextLS] Failed to start runtime for folder #{name}") + raise "Failed to boot runtime" + end - task = - Task.Supervisor.async_nolink(lsp.assigns.task_supervisor, fn -> - with false <- - wait_until(fn -> - NextLS.Runtime.ready?(runtime) - end) do - GenLSP.error(lsp, "[NextLS] Failed to start runtime") - raise "Failed to boot runtime" - end + GenLSP.log(lsp, "[NextLS] Runtime for folder #{name} is ready...") - GenLSP.log(lsp, "[NextLS] Runtime ready...") + {name, :ready} + end) + end - :ready - end) + refresh_refs = + Enum.zip_with(tasks, runtimes, fn task, {_name, runtime} -> {task.ref, runtime.refresh_ref} end) |> Map.new() {:noreply, assign(lsp, - refresh_refs: Map.put(lsp.assigns.refresh_refs, task.ref, {token, "NextLS runtime has initialized!"}), - runtime_task: task + refresh_refs: Map.merge(lsp.assigns.refresh_refs, refresh_refs), + runtime_tasks: tasks )} end @@ -322,23 +348,27 @@ defmodule NextLS do %{assigns: %{ready: true}} = lsp ) do for task <- Task.Supervisor.children(lsp.assigns.task_supervisor), - task != lsp.assigns.runtime_task.pid do + task not in for(t <- lsp.assigns.runtime_tasks, do: t.pid) do Process.exit(task, :kill) end token = token() progress_start(lsp, token, "Compiling...") + runtimes = Enum.to_list(lsp.assigns.runtimes) + + tasks = + for {name, r} <- runtimes do + Task.Supervisor.async_nolink(lsp.assigns.task_supervisor, fn -> {name, Runtime.compile(r.runtime)} end) + end - task = - Task.Supervisor.async_nolink(lsp.assigns.task_supervisor, fn -> - Runtime.compile(lsp.assigns.runtime) - end) + refresh_refs = + Enum.zip_with(tasks, runtimes, fn task, {_name, runtime} -> {task.ref, runtime.refresh_ref} end) |> Map.new() {:noreply, lsp |> then(&put_in(&1.assigns.documents[uri], String.split(text, "\n"))) - |> then(&put_in(&1.assigns.refresh_refs[task.ref], {token, "Compiled!"}))} + |> then(&put_in(&1.assigns.refresh_refs, refresh_refs))} end def handle_notification(%TextDocumentDidChange{}, %{assigns: %{ready: false}} = lsp) do @@ -355,7 +385,7 @@ defmodule NextLS do lsp ) do for task <- Task.Supervisor.children(lsp.assigns.task_supervisor), - task != lsp.assigns.runtime_task.pid do + task not in for(t <- lsp.assigns.runtime_tasks, do: t.pid) do Process.exit(task, :kill) end @@ -421,13 +451,13 @@ defmodule NextLS do lsp = case resp do - :ready -> + {name, :ready} -> token = token() progress_start(lsp, token, "Compiling...") task = Task.Supervisor.async_nolink(lsp.assigns.task_supervisor, fn -> - Runtime.compile(lsp.assigns.runtime) + {name, Runtime.compile(lsp.assigns.runtimes[name].runtime)} end) assign(lsp, ready: true, refresh_refs: Map.put(refs, task.ref, {token, "Compiled!"})) @@ -448,13 +478,11 @@ defmodule NextLS do {:noreply, assign(lsp, refresh_refs: refs)} end - def handle_info( - {:DOWN, _ref, :process, runtime, _reason}, - %{assigns: %{runtime: runtime}} = lsp - ) do - GenLSP.error(lsp, "[NextLS] The runtime has crashed") + def handle_info({:DOWN, _ref, :process, runtime, _reason}, %{assigns: %{runtimes: runtimes}} = lsp) do + {name, _} = Enum.find(runtimes, fn {_name, %{runtime: r}} -> r == runtime end) + GenLSP.error(lsp, "[NextLS] The runtime for #{name} has crashed") - {:noreply, assign(lsp, runtime: nil)} + {:noreply, assign(lsp, runtimes: Map.drop(runtimes, name))} end def handle_info(message, lsp) do diff --git a/test/next_ls_test.exs b/test/next_ls_test.exs index e7babb09..4c1725f6 100644 --- a/test/next_ls_test.exs +++ b/test/next_ls_test.exs @@ -60,14 +60,9 @@ defmodule NextLSTest do end test "responds correctly to a shutdown request", %{client: client} do - assert :ok == - notify(client, %{ - method: "initialized", - jsonrpc: "2.0", - params: %{} - }) + assert :ok == notify(client, %{method: "initialized", jsonrpc: "2.0", params: %{}}) - assert_notification "window/logMessage", %{"message" => "[NextLS] Runtime ready..."} + assert_notification "window/logMessage", %{"message" => "[NextLS] Runtime for folder my_proj is ready..."} assert :ok == request(client, %{ @@ -119,7 +114,7 @@ defmodule NextLSTest do "change" => 1 } }, - "serverInfo" => %{"name" => "NextLS"} + "serverInfo" => %{"name" => "Next LS"} } end @@ -136,12 +131,14 @@ defmodule NextLSTest do "type" => 4 } - assert_notification "$/progress", %{"value" => %{"kind" => "begin", "title" => "Initializing NextLS runtime..."}} + assert_notification "$/progress", %{ + "value" => %{"kind" => "begin", "title" => "Initializing NextLS runtime for folder my_proj..."} + } assert_notification "$/progress", %{ "value" => %{ "kind" => "end", - "message" => "NextLS runtime has initialized!" + "message" => "NextLS runtime for folder my_proj has initialized!" } } @@ -182,7 +179,7 @@ defmodule NextLSTest do end end - test "formats", %{client: client} do + test "formats", %{client: client, cwd: cwd} do assert :ok == notify(client, %{ method: "initialized", @@ -195,7 +192,7 @@ defmodule NextLSTest do jsonrpc: "2.0", params: %{ textDocument: %{ - uri: "file://lib/foo/bar.ex", + uri: "file://#{cwd}/lib/foo/bar.ex", languageId: "elixir", version: 1, text: """ @@ -217,7 +214,7 @@ defmodule NextLSTest do jsonrpc: "2.0", params: %{ textDocument: %{ - uri: "file://lib/foo/bar.ex" + uri: "file://#{cwd}/lib/foo/bar.ex" }, options: %{ insertSpaces: true, @@ -228,7 +225,7 @@ defmodule NextLSTest do assert_result 2, nil - assert_notification "window/logMessage", %{"message" => "[NextLS] Runtime ready..."} + assert_notification "window/logMessage", %{"message" => "[NextLS] Runtime for folder my_proj is ready..."} request client, %{ method: "textDocument/formatting", @@ -236,7 +233,7 @@ defmodule NextLSTest do jsonrpc: "2.0", params: %{ textDocument: %{ - uri: "file://lib/foo/bar.ex" + uri: "file://#{cwd}/lib/foo/bar.ex" }, options: %{ insertSpaces: true, @@ -261,7 +258,7 @@ defmodule NextLSTest do ] end - test "formatting gracefully handles files with syntax errors", %{client: client} do + test "formatting gracefully handles files with syntax errors", %{client: client, cwd: cwd} do assert :ok == notify(client, %{ method: "initialized", @@ -274,7 +271,7 @@ defmodule NextLSTest do jsonrpc: "2.0", params: %{ textDocument: %{ - uri: "file://lib/foo/bar.ex", + uri: "file://#{cwd}/lib/foo/bar.ex", languageId: "elixir", version: 1, text: """ @@ -289,7 +286,7 @@ defmodule NextLSTest do } } - assert_notification "window/logMessage", %{"message" => "[NextLS] Runtime ready..."} + assert_notification "window/logMessage", %{"message" => "[NextLS] Runtime for folder my_proj is ready..."} request client, %{ method: "textDocument/formatting", @@ -297,7 +294,7 @@ defmodule NextLSTest do jsonrpc: "2.0", params: %{ textDocument: %{ - uri: "file://lib/foo/bar.ex" + uri: "file://#{cwd}/lib/foo/bar.ex" }, options: %{ insertSpaces: true, @@ -317,7 +314,7 @@ defmodule NextLSTest do params: %{} }) - assert_notification "window/logMessage", %{"message" => "[NextLS] Runtime ready..."} + assert_notification "window/logMessage", %{"message" => "[NextLS] Runtime for folder my_proj is ready..."} assert_notification "window/logMessage", %{"message" => "[NextLS] Compiled!"} request client, %{ @@ -412,7 +409,7 @@ defmodule NextLSTest do params: %{} }) - assert_notification "window/logMessage", %{"message" => "[NextLS] Runtime ready..."} + assert_notification "window/logMessage", %{"message" => "[NextLS] Runtime for folder my_proj is ready..."} assert_notification "window/logMessage", %{"message" => "[NextLS] Compiled!"} request client, %{ @@ -517,7 +514,7 @@ defmodule NextLSTest do params: %{} }) - assert_notification "window/logMessage", %{"message" => "[NextLS] Runtime ready..."} + assert_notification "window/logMessage", %{"message" => "[NextLS] Runtime for folder my_proj is ready..."} assert_notification "window/logMessage", %{"message" => "[NextLS] Compiled!"} uri = uri(bar) @@ -555,7 +552,7 @@ defmodule NextLSTest do params: %{} }) - assert_notification "window/logMessage", %{"message" => "[NextLS] Runtime ready..."} + assert_notification "window/logMessage", %{"message" => "[NextLS] Runtime for folder my_proj is ready..."} assert_notification "window/logMessage", %{"message" => "[NextLS] Compiled!"} uri = uri(bar) @@ -595,7 +592,7 @@ defmodule NextLSTest do params: %{} }) - assert_notification "window/logMessage", %{"message" => "[NextLS] Runtime ready..."} + assert_notification "window/logMessage", %{"message" => "[NextLS] Runtime for folder my_proj is ready..."} assert_notification "window/logMessage", %{"message" => "[NextLS] Compiled!"} uri = uri(bar) @@ -689,7 +686,6 @@ defmodule NextLSTest do params: %{} }) - assert_notification "window/logMessage", %{"message" => "[NextLS] Runtime ready..."} assert_notification "window/logMessage", %{"message" => "[NextLS] Compiled!"} uri = uri(bar) @@ -727,7 +723,7 @@ defmodule NextLSTest do params: %{} }) - assert_notification "window/logMessage", %{"message" => "[NextLS] Runtime ready..."} + assert_notification "window/logMessage", %{"message" => "[NextLS] Runtime for folder my_proj is ready..."} assert_notification "window/logMessage", %{"message" => "[NextLS] Compiled!"} uri = uri(bar) @@ -767,7 +763,7 @@ defmodule NextLSTest do params: %{} }) - assert_notification "window/logMessage", %{"message" => "[NextLS] Runtime ready..."} + assert_notification "window/logMessage", %{"message" => "[NextLS] Runtime for folder my_proj is ready..."} assert_notification "window/logMessage", %{"message" => "[NextLS] Compiled!"} uri = uri(bar) @@ -830,7 +826,7 @@ defmodule NextLSTest do test "go to module definition", %{client: client, bar: bar, peace: peace} do assert :ok == notify(client, %{method: "initialized", jsonrpc: "2.0", params: %{}}) - assert_notification "window/logMessage", %{"message" => "[NextLS] Runtime ready..."} + assert_notification "window/logMessage", %{"message" => "[NextLS] Runtime for folder my_proj is ready..."} assert_notification "window/logMessage", %{"message" => "[NextLS] Compiled!"} uri = uri(bar) @@ -896,7 +892,17 @@ defmodule NextLSTest do method: "initialize", id: 1, jsonrpc: "2.0", - params: %{capabilities: %{}, rootUri: "file://#{root_path}"} + params: %{ + capabilities: %{ + workspace: %{ + workspaceFolders: true + } + }, + workspaceFolders: [ + %{uri: "file://#{root_path}", name: "my_proj"} + ], + rootUri: "file://#{root_path}" + } }) [server: server, client: client, cwd: root_path]