diff --git a/lib/next_ls.ex b/lib/next_ls.ex index b5a2b257..d3fc541f 100644 --- a/lib/next_ls.ex +++ b/lib/next_ls.ex @@ -77,7 +77,9 @@ defmodule NextLS do registry = Keyword.fetch!(args, :registry) - extensions = Keyword.get(args, :extensions, elixir: NextLS.ElixirExtension, credo: NextLS.CredoExtension) + extensions = + Keyword.get(args, :extensions, elixir: NextLS.ElixirExtension, credo: NextLS.CredoExtension) + cache = Keyword.fetch!(args, :cache) {:ok, logger} = DynamicSupervisor.start_child(dynamic_supervisor, {NextLS.Logger, lsp: lsp}) @@ -179,7 +181,11 @@ defmodule NextLS do ) do code_actions = for %Diagnostic{} = diagnostic <- diagnostics, - data = %NextLS.CodeActionable.Data{diagnostic: diagnostic, uri: uri, document: lsp.assigns.documents[uri]}, + data = %NextLS.CodeActionable.Data{ + diagnostic: diagnostic, + uri: uri, + document: lsp.assigns.documents[uri] + }, namespace = diagnostic.data["namespace"], action <- NextLS.CodeActionable.from(namespace, data) do action @@ -192,7 +198,11 @@ defmodule NextLS do result = dispatch(lsp.assigns.registry, :databases, fn entries -> for {pid, _} <- entries do - case Definition.fetch(URI.parse(uri).path, {position.line + 1, position.character + 1}, pid) do + case Definition.fetch( + URI.parse(uri).path, + {position.line + 1, position.character + 1}, + pid + ) do nil -> case NextLS.ASTHelpers.Variables.get_variable_definition( URI.parse(uri).path, @@ -243,14 +253,14 @@ defmodule NextLS do def handle_request(%TextDocumentDocumentSymbol{params: %{text_document: %{uri: uri}}}, lsp) do symbols = - if Path.extname(uri) in [".ex", ".exs"] do + if Path.extname(uri) in [".ex", ".exs"] && lsp.assigns.documents[uri] do try do lsp.assigns.documents[uri] |> Enum.join("\n") |> NextLS.DocumentSymbol.fetch() rescue e -> - GenLSP.error(lsp, Exception.format_banner(:error, e, __STACKTRACE__)) + GenLSP.error(lsp, Exception.format(:error, e, __STACKTRACE__)) nil end else @@ -321,7 +331,8 @@ defmodule NextLS do end) end - for [file, startl, endl, startc, endc] <- references, match?({:ok, _}, File.stat(file)) do + for [file, startl, endl, startc, endc] <- references, + match?({:ok, _}, File.stat(file)) do %Location{ uri: "file://#{file}", range: %Range{ @@ -422,7 +433,10 @@ defmodule NextLS do value: String.trim(value) }, range: %Range{ - start: %Position{line: reference.start_line - 1, character: reference.start_column - 1}, + start: %Position{ + line: reference.start_line - 1, + character: reference.start_column - 1 + }, end: %Position{line: reference.end_line - 1, character: reference.end_column - 1} } } @@ -465,7 +479,9 @@ defmodule NextLS do symbols = dispatch(lsp.assigns.registry, :databases, fn entries -> filtered_symbols = - for {pid, _} <- entries, symbol <- symbols.(pid), score = fuzzy_match(symbol.name, query, case_sensitive?) do + for {pid, _} <- entries, + symbol <- symbols.(pid), + score = fuzzy_match(symbol.name, query, case_sensitive?) do name = if symbol.type not in ["defstruct", "attribute"] do "#{symbol.type} #{symbol.name}" @@ -506,9 +522,15 @@ defmodule NextLS do dispatch(lsp.assigns.registry, :runtimes, fn entries -> for {runtime, %{uri: wuri}} <- entries, String.starts_with?(uri, wuri) do with {:ok, {formatter, _}} <- - Runtime.call(runtime, {Mix.Tasks.Format, :formatter_for_file, [URI.parse(uri).path]}), + Runtime.call( + runtime, + {:_next_ls_private_formatter, :formatter_for_file, [URI.parse(uri).path]} + ), {:ok, response} when is_binary(response) or is_list(response) <- - Runtime.call(runtime, {Kernel, :apply, [formatter, [Enum.join(document, "\n")]]}) do + Runtime.call( + runtime, + {Kernel, :apply, [formatter, [Enum.join(document, "\n")]]} + ) do {:reply, [ %TextEdit{ @@ -584,7 +606,11 @@ defmodule NextLS do dispatch(lsp.assigns.registry, :runtimes, fn entries -> [{wuri, result}] = for {runtime, %{uri: wuri}} <- entries, String.starts_with?(uri, wuri) do - {wuri, document_slice |> String.to_charlist() |> Enum.reverse() |> NextLS.Autocomplete.expand(runtime, env)} + {wuri, + document_slice + |> String.to_charlist() + |> Enum.reverse() + |> NextLS.Autocomplete.expand(runtime, env)} end case result do @@ -598,14 +624,29 @@ defmodule NextLS do |> Enum.reduce([], fn %{name: name, kind: kind} = symbol, results -> {label, kind, docs} = case kind do - :struct -> {name, GenLSP.Enumerations.CompletionItemKind.struct(), ""} - :function -> {"#{name}/#{symbol.arity}", GenLSP.Enumerations.CompletionItemKind.function(), symbol.docs} - :module -> {name, GenLSP.Enumerations.CompletionItemKind.module(), ""} - :variable -> {name, GenLSP.Enumerations.CompletionItemKind.variable(), ""} - :dir -> {name, GenLSP.Enumerations.CompletionItemKind.folder(), ""} - :file -> {name, GenLSP.Enumerations.CompletionItemKind.file(), ""} - :keyword -> {name, GenLSP.Enumerations.CompletionItemKind.field(), ""} - _ -> {name, GenLSP.Enumerations.CompletionItemKind.text(), ""} + :struct -> + {name, GenLSP.Enumerations.CompletionItemKind.struct(), ""} + + :function -> + {"#{name}/#{symbol.arity}", GenLSP.Enumerations.CompletionItemKind.function(), symbol.docs} + + :module -> + {name, GenLSP.Enumerations.CompletionItemKind.module(), ""} + + :variable -> + {name, GenLSP.Enumerations.CompletionItemKind.variable(), ""} + + :dir -> + {name, GenLSP.Enumerations.CompletionItemKind.folder(), ""} + + :file -> + {name, GenLSP.Enumerations.CompletionItemKind.file(), ""} + + :keyword -> + {name, GenLSP.Enumerations.CompletionItemKind.field(), ""} + + _ -> + {name, GenLSP.Enumerations.CompletionItemKind.text(), ""} end completion_item = @@ -671,7 +712,12 @@ defmodule NextLS do }) _ -> - NextLS.Logger.show_message(lsp.logger, :warning, "[Next LS] Unknown workspace command: #{command}") + NextLS.Logger.show_message( + lsp.logger, + :warning, + "[Next LS] Unknown workspace command: #{command}" + ) + nil end @@ -695,7 +741,7 @@ defmodule NextLS do "[Next LS] #{command} has failed, see the logs for more details" ) - NextLS.Logger.error(lsp.assigns.logger, Exception.format_banner(:error, e, __STACKTRACE__)) + NextLS.Logger.error(lsp.assigns.logger, Exception.format(:error, e, __STACKTRACE__)) {:reply, nil, lsp} end @@ -745,7 +791,8 @@ defmodule NextLS do end end - with %{dynamic_registration: true} <- lsp.assigns.client_capabilities.workspace.did_change_watched_files do + with %{dynamic_registration: true} <- + lsp.assigns.client_capabilities.workspace.did_change_watched_files do nil = GenLSP.request(lsp, %GenLSP.Requests.ClientRegisterCapability{ id: System.unique_integer([:positive]), @@ -782,6 +829,7 @@ defmodule NextLS do path: Path.join(working_dir, ".elixir-tools"), name: name, lsp: lsp, + lsp_pid: parent, registry: lsp.assigns.registry, logger: lsp.assigns.logger, runtime: [ @@ -835,23 +883,25 @@ defmodule NextLS do refresh_refs = dispatch(lsp.assigns.registry, :runtimes, fn entries -> - for {pid, %{name: name, uri: wuri}} <- entries, String.starts_with?(uri, wuri), into: %{} do + for {pid, %{name: name, uri: wuri}} <- entries, + String.starts_with?(uri, wuri), + into: %{} do token = Progress.token() Progress.start(lsp, token, "Compiling #{name}...") - task = - Task.Supervisor.async_nolink(lsp.assigns.task_supervisor, fn -> - {name, Runtime.compile(pid)} - end) + ref = make_ref() + Runtime.compile(pid, caller_ref: ref) - {task.ref, {token, "Compiled #{name}!"}} + {ref, {token, "Compiled #{name}!"}} end end) - {:noreply, - lsp - |> then(&put_in(&1.assigns.documents[uri], String.split(text, "\n"))) - |> then(&put_in(&1.assigns.refresh_refs, refresh_refs))} + lsp = + lsp + |> then(&put_in(&1.assigns.documents[uri], String.split(text, "\n"))) + |> then(&update_in(&1.assigns.refresh_refs, fn r -> Map.merge(r, refresh_refs) end)) + + {:noreply, lsp} end def handle_notification(%TextDocumentDidChange{}, %{assigns: %{ready: false}} = lsp) do @@ -866,7 +916,8 @@ defmodule NextLS do Process.exit(task, :kill) end - {:noreply, put_in(lsp.assigns.documents[uri], String.split(text, "\n"))} + lsp = put_in(lsp.assigns.documents[uri], String.split(text, "\n")) + {:noreply, lsp} end def handle_notification( @@ -898,6 +949,8 @@ defmodule NextLS do NextLS.Runtime.boot(lsp.assigns.dynamic_supervisor, path: Path.join(working_dir, ".elixir-tools"), name: name, + lsp: lsp, + lsp_pid: parent, registry: lsp.assigns.registry, runtime: [ task_supervisor: lsp.assigns.runtime_task_supervisor, @@ -942,36 +995,51 @@ defmodule NextLS do end def handle_notification(%WorkspaceDidChangeWatchedFiles{params: %DidChangeWatchedFilesParams{changes: changes}}, lsp) do - type = GenLSP.Enumerations.FileChangeType.deleted() - lsp = - for %{type: ^type, uri: uri} <- changes, reduce: lsp do + for %{type: type, uri: uri} <- changes, reduce: lsp do lsp -> - dispatch(lsp.assigns.registry, :databases, fn entries -> - for {pid, _} <- entries do - file = URI.parse(uri).path - - NextLS.DB.query( - pid, - ~Q""" - DELETE FROM symbols - WHERE symbols.file = ?; - """, - [file] - ) + cond do + type == GenLSP.Enumerations.FileChangeType.created() -> + with {:ok, text} <- File.read(URI.parse(uri).path) do + put_in(lsp.assigns.documents[uri], String.split(text, "\n")) + else + _ -> lsp + end - NextLS.DB.query( - pid, - ~Q""" - DELETE FROM 'references' AS refs - WHERE refs.file = ?; - """, - [file] - ) - end - end) + type == GenLSP.Enumerations.FileChangeType.changed() -> + with {:ok, text} <- File.read(URI.parse(uri).path) do + put_in(lsp.assigns.documents[uri], String.split(text, "\n")) + else + _ -> lsp + end + + type == GenLSP.Enumerations.FileChangeType.deleted() -> + dispatch(lsp.assigns.registry, :databases, fn entries -> + for {pid, _} <- entries do + file = URI.parse(uri).path + + NextLS.DB.query( + pid, + ~Q""" + DELETE FROM symbols + WHERE symbols.file = ?; + """, + [file] + ) + + NextLS.DB.query( + pid, + ~Q""" + DELETE FROM 'references' AS refs + WHERE refs.file = ?; + """, + [file] + ) + end + end) - update_in(lsp.assigns.documents, &Map.drop(&1, [uri])) + update_in(lsp.assigns.documents, &Map.drop(&1, [uri])) + end end {:noreply, lsp} @@ -984,24 +1052,65 @@ defmodule NextLS do end def handle_notification(_notification, lsp) do + # dbg("unhandled notification") + # dbg(notification) + {:noreply, lsp} end - def handle_info(:publish, lsp) do - all = - for {_namespace, cache} <- DiagnosticCache.get(lsp.assigns.cache), {file, diagnostics} <- cache, reduce: %{} do - d -> Map.update(d, file, diagnostics, fn value -> value ++ diagnostics end) - end + def handle_info({:compiler_result, caller_ref, name, result} = _msg, lsp) do + {{token, msg}, refs} = Map.pop(lsp.assigns.refresh_refs, caller_ref) - for {file, diagnostics} <- all do - GenLSP.notify(lsp, %GenLSP.Notifications.TextDocumentPublishDiagnostics{ - params: %GenLSP.Structures.PublishDiagnosticsParams{ - uri: "file://#{file}", - diagnostics: diagnostics - } - }) + case result do + {_, diagnostics} when is_list(diagnostics) -> + Registry.dispatch(lsp.assigns.registry, :extensions, fn entries -> + for {pid, _} <- entries, do: send(pid, {:compiler, diagnostics}) + end) + + NextLS.Logger.info(lsp.assigns.logger, "Compiled #{name}!") + + {:error, %Mix.Error{message: "Can't continue due to errors on dependencies"}} -> + send(self(), {:runtime_failed, name, {:error, :deps}}) + + unknown -> + NextLS.Logger.warning( + lsp.assigns.logger, + "Unexpected compiler response: #{inspect(unknown)}" + ) end + Progress.stop(lsp, token, msg) + + {:noreply, assign(lsp, refresh_refs: refs)} + end + + def handle_info({:compiler_canceled, caller_ref}, lsp) do + {{token, msg}, refs} = Map.pop(lsp.assigns.refresh_refs, caller_ref) + + Progress.stop(lsp, token, msg) + + {:noreply, assign(lsp, refresh_refs: refs)} + end + + def handle_info(:publish, lsp) do + Task.start(fn -> + all = + for {_namespace, cache} <- DiagnosticCache.get(lsp.assigns.cache), + {file, diagnostics} <- cache, + reduce: %{} do + d -> Map.update(d, file, diagnostics, fn value -> value ++ diagnostics end) + end + + for {file, diagnostics} <- all do + GenLSP.notify(lsp, %GenLSP.Notifications.TextDocumentPublishDiagnostics{ + params: %GenLSP.Structures.PublishDiagnosticsParams{ + uri: "file://#{file}", + diagnostics: diagnostics + } + }) + end + end) + {:noreply, lsp} end @@ -1009,17 +1118,15 @@ defmodule NextLS do token = Progress.token() Progress.start(lsp, token, "Compiling #{name}...") - task = - Task.Supervisor.async_nolink(lsp.assigns.task_supervisor, fn -> - {_, %{mode: mode}} = - dispatch(lsp.assigns.registry, :databases, fn entries -> - Enum.find(entries, fn {_, %{runtime: runtime}} -> runtime == name end) - end) - - {name, Runtime.compile(runtime_pid, force: mode == :reindex)} + {_, %{mode: mode}} = + dispatch(lsp.assigns.registry, :databases, fn entries -> + Enum.find(entries, fn {_, %{runtime: runtime}} -> runtime == name end) end) - refresh_refs = Map.put(lsp.assigns.refresh_refs, task.ref, {token, "Compiled #{name}!"}) + ref = make_ref() + Runtime.compile(runtime_pid, caller_ref: ref, force: mode == :reindex) + + refresh_refs = Map.put(lsp.assigns.refresh_refs, ref, {token, "Compiled #{name}!"}) {:noreply, assign(lsp, ready: true, refresh_refs: refresh_refs)} end @@ -1073,7 +1180,10 @@ defmodule NextLS do NextLS.Logger.info(lsp.assigns.logger, msg) {:ok, _} = - DynamicSupervisor.start_child(lsp.assigns.dynamic_supervisor, {NextLS.Runtime.Supervisor, init_arg}) + DynamicSupervisor.start_child( + lsp.assigns.dynamic_supervisor, + {NextLS.Runtime.Supervisor, init_arg} + ) {msg, _} -> NextLS.Logger.warning( @@ -1101,26 +1211,9 @@ defmodule NextLS do {:noreply, assign(lsp, refresh_refs: refs)} end - def handle_info({ref, _resp}, %{assigns: %{refresh_refs: refs}} = lsp) when is_map_key(refs, ref) do - Process.demonitor(ref, [:flush]) - {{token, msg}, refs} = Map.pop(refs, ref) - - Progress.stop(lsp, token, msg) - - {:noreply, assign(lsp, refresh_refs: refs)} - end - - def handle_info({:DOWN, ref, :process, _pid, _reason}, %{assigns: %{refresh_refs: refs}} = lsp) - when is_map_key(refs, ref) do - {{token, _}, refs} = Map.pop(refs, ref) - - Progress.stop(lsp, token) - - {:noreply, assign(lsp, refresh_refs: refs)} - end - def handle_info(message, lsp) do GenLSP.log(lsp, "[Next LS] Unhandled message: #{inspect(message)}") + GenLSP.log(lsp, "[Next LS] process assigns=#{inspect(lsp.assigns)}") {:noreply, lsp} end @@ -1257,7 +1350,8 @@ defmodule NextLS do defp calc_match_score(_source_letters, [], _traits, score), do: score defp calc_match_score(source_letters, [query_letter | query_rest], traits, score) do - {rest_source_letters, new_traits, new_score} = calc_letter_score(source_letters, query_letter, traits, score) + {rest_source_letters, new_traits, new_score} = + calc_letter_score(source_letters, query_letter, traits, score) calc_match_score(rest_source_letters, query_rest, new_traits, new_score) end diff --git a/lib/next_ls/progress.ex b/lib/next_ls/progress.ex index 74572184..4e26e305 100644 --- a/lib/next_ls/progress.ex +++ b/lib/next_ls/progress.ex @@ -2,26 +2,28 @@ defmodule NextLS.Progress do @moduledoc false @env Mix.env() def start(lsp, token, msg) do - # FIXME: gen_lsp should allow stubbing requests so we don't have to - # set this in every test. For now, don't send it in the test env - if @env != :test do - GenLSP.request(lsp, %GenLSP.Requests.WindowWorkDoneProgressCreate{ - id: System.unique_integer([:positive]), - params: %GenLSP.Structures.WorkDoneProgressCreateParams{ - token: token - } - }) - end + Task.start(fn -> + # FIXME: gen_lsp should allow stubbing requests so we don't have to + # set this in every test. For now, don't send it in the test env + if @env != :test do + GenLSP.request(lsp, %GenLSP.Requests.WindowWorkDoneProgressCreate{ + id: System.unique_integer([:positive]), + params: %GenLSP.Structures.WorkDoneProgressCreateParams{ + token: token + } + }) + end - GenLSP.notify(lsp, %GenLSP.Notifications.DollarProgress{ - params: %GenLSP.Structures.ProgressParams{ - token: token, - value: %GenLSP.Structures.WorkDoneProgressBegin{ - kind: "begin", - title: msg + GenLSP.notify(lsp, %GenLSP.Notifications.DollarProgress{ + params: %GenLSP.Structures.ProgressParams{ + token: token, + value: %GenLSP.Structures.WorkDoneProgressBegin{ + kind: "begin", + title: msg + } } - } - }) + }) + end) end def stop(lsp, token, msg \\ nil) do diff --git a/lib/next_ls/runtime.ex b/lib/next_ls/runtime.ex index 27ee3165..8877bfbb 100644 --- a/lib/next_ls/runtime.ex +++ b/lib/next_ls/runtime.ex @@ -90,6 +90,7 @@ defmodule NextLS.Runtime do sname = "nextls-runtime-#{System.system_time()}" name = Keyword.fetch!(opts, :name) working_dir = Keyword.fetch!(opts, :working_dir) + lsp_pid = Keyword.fetch!(opts, :lsp_pid) uri = Keyword.fetch!(opts, :uri) parent = Keyword.fetch!(opts, :parent) logger = Keyword.fetch!(opts, :logger) @@ -222,6 +223,8 @@ defmodule NextLS.Runtime do :ok end) + {:ok, _} = :rpc.call(node, :_next_ls_private_compiler, :start, []) + send(me, {:node, node}) else error -> @@ -237,6 +240,7 @@ defmodule NextLS.Runtime do port: port, task_supervisor: task_supervisor, logger: logger, + lsp_pid: lsp_pid, parent: parent, errors: nil, registry: registry, @@ -279,54 +283,32 @@ defmodule NextLS.Runtime do end end - def handle_call({:compile, opts}, from, %{node: node} = state) do - for {_ref, {task_pid, _from}} <- state.compiler_refs, do: Process.exit(task_pid, :kill) - - task = - Task.Supervisor.async_nolink(state.task_supervisor, fn -> - if opts[:force] do - File.rm_rf!(Path.join(state.working_dir, ".elixir-tools/_build")) - end - - case :rpc.call(node, :_next_ls_private_compiler, :compile, []) do - {:badrpc, error} -> - NextLS.Logger.error(state.logger, "Bad RPC call to node #{node}: #{inspect(error)}") - [] - - {_, diagnostics} when is_list(diagnostics) -> - Registry.dispatch(state.registry, :extensions, fn entries -> - for {pid, _} <- entries, do: send(pid, {:compiler, diagnostics}) - end) - - NextLS.Logger.info(state.logger, "Compiled #{state.name}!") - - diagnostics - - {:error, %Mix.Error{message: "Can't continue due to errors on dependencies"}} -> - {:runtime_failed, state.name, {:error, :deps}} + def handle_call({:compile, opts}, _from, %{node: node} = state) do + opts = + opts + |> Keyword.put_new(:working_dir, state.working_dir) + |> Keyword.put_new(:registry, state.registry) + |> Keyword.put(:from, self()) - unknown -> - NextLS.Logger.warning(state.logger, "Unexpected compiler response: #{inspect(unknown)}") - [] - end - end) + with {:badrpc, error} <- :rpc.call(node, :_next_ls_private_compiler_worker, :enqueue_compiler, [opts]) do + NextLS.Logger.error(state.logger, "Bad RPC call to node #{node}: #{inspect(error)}") + end - {:noreply, %{state | compiler_refs: Map.put(state.compiler_refs, task.ref, {task.pid, from})}} + {:reply, :ok, state} end @impl GenServer - def handle_info({ref, errors}, %{compiler_refs: compiler_refs} = state) when is_map_key(compiler_refs, ref) do - Process.demonitor(ref, [:flush]) - - orig = elem(compiler_refs[ref], 1) - GenServer.reply(orig, errors) - - {:noreply, %{state | compiler_refs: Map.delete(compiler_refs, ref)}} + # NOTE: these two callbacks are basically to forward the messages from the runtime to the LSP + # LSP process so that progress messages can be dispatched + def handle_info({:compiler_result, caller_ref, result}, state) do + # we add the runtime name into the message + send(state.lsp_pid, {:compiler_result, caller_ref, state.name, result}) + {:noreply, state} end - def handle_info({:DOWN, ref, :process, _pid, _reason}, %{compiler_refs: compiler_refs} = state) - when is_map_key(compiler_refs, ref) do - {:noreply, %{state | compiler_refs: Map.delete(compiler_refs, ref)}} + def handle_info({:compiler_canceled, _caller_ref} = msg, state) do + send(state.lsp_pid, msg) + {:noreply, state} end def handle_info({:DOWN, _, :port, port, _}, %{port: port} = state) do diff --git a/lib/next_ls/runtime/supervisor.ex b/lib/next_ls/runtime/supervisor.ex index 7ccb4d88..091d8e30 100644 --- a/lib/next_ls/runtime/supervisor.ex +++ b/lib/next_ls/runtime/supervisor.ex @@ -11,6 +11,7 @@ defmodule NextLS.Runtime.Supervisor do def init(init_arg) do name = init_arg[:name] lsp = init_arg[:lsp] + lsp_pid = init_arg[:lsp_pid] registry = init_arg[:registry] logger = init_arg[:logger] hidden_folder = init_arg[:path] @@ -34,7 +35,8 @@ defmodule NextLS.Runtime.Supervisor do name: db_name, runtime: name, activity: db_activity}, - {NextLS.Runtime, init_arg[:runtime] ++ [name: name, registry: registry, parent: sidecar_name, db: db_name]} + {NextLS.Runtime, + init_arg[:runtime] ++ [name: name, registry: registry, parent: sidecar_name, lsp_pid: lsp_pid, db: db_name]} ] Supervisor.init(children, strategy: :one_for_one) diff --git a/priv/monkey/_next_ls_private_compiler.ex b/priv/monkey/_next_ls_private_compiler.ex index 51017d3d..c8e075e7 100644 --- a/priv/monkey/_next_ls_private_compiler.ex +++ b/priv/monkey/_next_ls_private_compiler.ex @@ -220,9 +220,834 @@ defmodule NextLSPrivate.Tracer do end end +# vendored from Elixir, Apache 2 license +defmodule :_next_ls_private_formatter do + @moduledoc false + @switches [ + check_equivalent: :boolean, + check_formatted: :boolean, + no_exit: :boolean, + dot_formatter: :string, + dry_run: :boolean, + stdin_filename: :string + ] + + @manifest "cached_dot_formatter" + @manifest_vsn 2 + + @newline "\n" + @blank " " + + @separator "|" + @cr "↵" + @line_num_pad @blank + + @gutter [ + del: " -", + eq: " ", + ins: " +", + skip: " " + ] + + @colors [ + del: [text: :red, space: :red_background], + ins: [text: :green, space: :green_background] + ] + + @callback features(Keyword.t()) :: [sigils: [atom()], extensions: [binary()]] + + @callback format(String.t(), Keyword.t()) :: String.t() + + @impl true + def run(args) do + cwd = File.cwd!() + {opts, args} = OptionParser.parse!(args, strict: @switches) + {dot_formatter, formatter_opts} = eval_dot_formatter(cwd, opts) + + if opts[:check_equivalent] do + IO.warn("--check-equivalent has been deprecated and has no effect") + end + + if opts[:no_exit] && !opts[:check_formatted] do + Mix.raise("--no-exit can only be used together with --check-formatted") + end + + {formatter_opts_and_subs, _sources} = + eval_deps_and_subdirectories(cwd, dot_formatter, formatter_opts, [dot_formatter]) + + formatter_opts_and_subs = load_plugins(formatter_opts_and_subs) + + args + |> expand_args(cwd, dot_formatter, formatter_opts_and_subs, opts) + |> Task.async_stream(&format_file(&1, opts), ordered: false, timeout: :infinity) + |> Enum.reduce({[], []}, &collect_status/2) + |> check!(opts) + end + + defp load_plugins({formatter_opts, subs}) do + plugins = Keyword.get(formatter_opts, :plugins, []) + + if not is_list(plugins) do + Mix.raise("Expected :plugins to return a list of modules, got: #{inspect(plugins)}") + end + + # if plugins != [] do + # Mix.Task.run("loadpaths", []) + # end + + # if not Enum.all?(plugins, &Code.ensure_loaded?/1) do + # Mix.Task.run("compile", []) + # end + + for plugin <- plugins do + cond do + not Code.ensure_loaded?(plugin) -> + Mix.raise("Formatter plugin #{inspect(plugin)} cannot be found") + + not function_exported?(plugin, :features, 1) -> + Mix.raise("Formatter plugin #{inspect(plugin)} does not define features/1") + + true -> + :ok + end + end + + sigils = + for plugin <- plugins, + sigil <- find_sigils_from_plugins(plugin, formatter_opts), + do: {sigil, plugin} + + sigils = + sigils + |> Enum.group_by(&elem(&1, 0), &elem(&1, 1)) + |> Enum.map(fn {sigil, plugins} -> + {sigil, + fn input, opts -> + Enum.reduce(plugins, input, fn plugin, input -> + plugin.format(input, opts ++ formatter_opts) + end) + end} + end) + + {Keyword.put(formatter_opts, :sigils, sigils), Enum.map(subs, fn {path, opts} -> {path, load_plugins(opts)} end)} + end + + def formatter_for_file(file, opts \\ []) do + cwd = Keyword.get_lazy(opts, :root, &File.cwd!/0) + {dot_formatter, formatter_opts} = eval_dot_formatter(cwd, opts) + + {formatter_opts_and_subs, _sources} = + eval_deps_and_subdirectories(cwd, dot_formatter, formatter_opts, [dot_formatter]) + + formatter_opts_and_subs = load_plugins(formatter_opts_and_subs) + + find_formatter_and_opts_for_file(Path.expand(file, cwd), formatter_opts_and_subs) + end + + defp eval_dot_formatter(cwd, opts) do + cond do + dot_formatter = opts[:dot_formatter] -> + {dot_formatter, eval_file_with_keyword_list(dot_formatter)} + + File.regular?(Path.join(cwd, ".formatter.exs")) -> + dot_formatter = Path.join(cwd, ".formatter.exs") + {".formatter.exs", eval_file_with_keyword_list(dot_formatter)} + + true -> + {".formatter.exs", []} + end + end + + # This function reads exported configuration from the imported + # dependencies and subdirectories and deals with caching the result + # of reading such configuration in a manifest file. + defp eval_deps_and_subdirectories(cwd, dot_formatter, formatter_opts, sources) do + deps = Keyword.get(formatter_opts, :import_deps, []) + subs = Keyword.get(formatter_opts, :subdirectories, []) + + if not is_list(deps) do + Mix.raise("Expected :import_deps to return a list of dependencies, got: #{inspect(deps)}") + end + + if not is_list(subs) do + Mix.raise("Expected :subdirectories to return a list of directories, got: #{inspect(subs)}") + end + + if deps == [] and subs == [] do + {{formatter_opts, []}, sources} + else + manifest = Path.join(Mix.Project.manifest_path(), @manifest) + + {{locals_without_parens, subdirectories}, sources} = + maybe_cache_in_manifest(dot_formatter, manifest, fn -> + {subdirectories, sources} = eval_subs_opts(subs, cwd, sources) + {{eval_deps_opts(deps), subdirectories}, sources} + end) + + formatter_opts = + Keyword.update( + formatter_opts, + :locals_without_parens, + locals_without_parens, + &(locals_without_parens ++ &1) + ) + + {{formatter_opts, subdirectories}, sources} + end + end + + defp maybe_cache_in_manifest(dot_formatter, manifest, fun) do + cond do + is_nil(Mix.Project.get()) or dot_formatter != ".formatter.exs" -> fun.() + entry = read_manifest(manifest) -> entry + true -> write_manifest!(manifest, fun.()) + end + end + + defp read_manifest(manifest) do + with {:ok, binary} <- File.read(manifest), + {:ok, {@manifest_vsn, entry, sources}} <- safe_binary_to_term(binary), + expanded_sources = Enum.flat_map(sources, &Path.wildcard(&1, match_dot: true)), + false <- Mix.Utils.stale?([Mix.Project.config_mtime() | expanded_sources], [manifest]) do + {entry, sources} + else + _ -> nil + end + end + + defp safe_binary_to_term(binary) do + {:ok, :erlang.binary_to_term(binary)} + rescue + _ -> :error + end + + defp write_manifest!(manifest, {entry, sources}) do + File.mkdir_p!(Path.dirname(manifest)) + File.write!(manifest, :erlang.term_to_binary({@manifest_vsn, entry, sources})) + {entry, sources} + end + + defp eval_deps_opts([]) do + [] + end + + defp eval_deps_opts(deps) do + deps_paths = Mix.Project.deps_paths() + + for dep <- deps, + dep_path = assert_valid_dep_and_fetch_path(dep, deps_paths), + dep_dot_formatter = Path.join(dep_path, ".formatter.exs"), + File.regular?(dep_dot_formatter), + dep_opts = eval_file_with_keyword_list(dep_dot_formatter), + parenless_call <- dep_opts[:export][:locals_without_parens] || [], + uniq: true, + do: parenless_call + end + + defp eval_subs_opts(subs, cwd, sources) do + {subs, sources} = + Enum.flat_map_reduce(subs, sources, fn sub, sources -> + cwd = Path.expand(sub, cwd) + {Path.wildcard(cwd), [Path.join(cwd, ".formatter.exs") | sources]} + end) + + Enum.flat_map_reduce(subs, sources, fn sub, sources -> + sub_formatter = Path.join(sub, ".formatter.exs") + + if File.exists?(sub_formatter) do + formatter_opts = eval_file_with_keyword_list(sub_formatter) + + {formatter_opts_and_subs, sources} = + eval_deps_and_subdirectories(sub, :in_memory, formatter_opts, sources) + + {[{sub, formatter_opts_and_subs}], sources} + else + {[], sources} + end + end) + end + + defp assert_valid_dep_and_fetch_path(dep, deps_paths) when is_atom(dep) do + with %{^dep => path} <- deps_paths, + true <- File.dir?(path) do + path + else + _ -> + Mix.raise( + "Unknown dependency #{inspect(dep)} given to :import_deps in the formatter configuration. " <> + "Make sure the dependency is listed in your mix.exs for environment #{inspect(Mix.env())} " <> + "and you have run \"mix deps.get\"" + ) + end + end + + defp assert_valid_dep_and_fetch_path(dep, _deps_paths) do + Mix.raise("Dependencies in :import_deps should be atoms, got: #{inspect(dep)}") + end + + defp eval_file_with_keyword_list(path) do + {opts, _} = Code.eval_file(path) + + unless Keyword.keyword?(opts) do + Mix.raise("Expected #{inspect(path)} to return a keyword list, got: #{inspect(opts)}") + end + + opts + end + + defp expand_args([], cwd, dot_formatter, formatter_opts_and_subs, _opts) do + if no_entries_in_formatter_opts?(formatter_opts_and_subs) do + Mix.raise( + "Expected one or more files/patterns to be given to mix format " <> + "or for a .formatter.exs file to exist with an :inputs or :subdirectories key" + ) + end + + dot_formatter + |> expand_dot_inputs(cwd, formatter_opts_and_subs, %{}) + |> Enum.map(fn {file, {_dot_formatter, formatter_opts}} -> + {file, find_formatter_for_file(file, formatter_opts)} + end) + end + + defp expand_args(files_and_patterns, cwd, _dot_formatter, {formatter_opts, subs}, opts) do + files = + for file_or_pattern <- files_and_patterns, + file <- stdin_or_wildcard(file_or_pattern), + uniq: true, + do: file + + if files == [] do + Mix.raise( + "Could not find a file to format. The files/patterns given to command line " <> + "did not point to any existing file. Got: #{inspect(files_and_patterns)}" + ) + end + + for file <- files do + if file == :stdin do + stdin_filename = Path.expand(Keyword.get(opts, :stdin_filename, "stdin.exs"), cwd) + + {formatter, _opts} = + find_formatter_and_opts_for_file(stdin_filename, {formatter_opts, subs}) + + {file, formatter} + else + {formatter, _opts} = find_formatter_and_opts_for_file(file, {formatter_opts, subs}) + {file, formatter} + end + end + end + + defp expand_dot_inputs(dot_formatter, cwd, {formatter_opts, subs}, acc) do + if no_entries_in_formatter_opts?({formatter_opts, subs}) do + Mix.raise("Expected :inputs or :subdirectories key in #{dot_formatter}") + end + + map = + for input <- List.wrap(formatter_opts[:inputs]), + file <- Path.wildcard(Path.expand(input, cwd), match_dot: true), + do: {file, {dot_formatter, formatter_opts}}, + into: %{} + + acc = + Map.merge(acc, map, fn file, {dot_formatter1, _}, {dot_formatter2, formatter_opts} -> + Mix.shell().error( + "Both #{dot_formatter1} and #{dot_formatter2} specify the file #{file} in their " <> + ":inputs option. To resolve the conflict, the configuration in #{dot_formatter1} " <> + "will be ignored. Please change the list of :inputs in one of the formatter files " <> + "so only one of them matches #{file}" + ) + + {dot_formatter2, formatter_opts} + end) + + Enum.reduce(subs, acc, fn {sub, formatter_opts_and_subs}, acc -> + sub_formatter = Path.join(sub, ".formatter.exs") + expand_dot_inputs(sub_formatter, sub, formatter_opts_and_subs, acc) + end) + end + + defp find_formatter_for_file(file, formatter_opts) do + ext = Path.extname(file) + + cond do + plugins = find_plugins_for_extension(formatter_opts, ext) -> + fn input -> + Enum.reduce(plugins, input, fn plugin, input -> + plugin.format(input, [extension: ext, file: file] ++ formatter_opts) + end) + end + + ext in ~w(.ex .exs) -> + &elixir_format(&1, [file: file] ++ formatter_opts) + + true -> + & &1 + end + end + + defp find_plugins_for_extension(formatter_opts, ext) do + plugins = Keyword.get(formatter_opts, :plugins, []) + + plugins = + Enum.filter(plugins, fn plugin -> + Code.ensure_loaded?(plugin) and function_exported?(plugin, :features, 1) and + ext in List.wrap(plugin.features(formatter_opts)[:extensions]) + end) + + if plugins != [], do: plugins, else: nil + end + + defp find_formatter_and_opts_for_file(file, formatter_opts_and_subs) do + formatter_opts = recur_formatter_opts_for_file(file, formatter_opts_and_subs) + {find_formatter_for_file(file, formatter_opts), formatter_opts} + end + + defp recur_formatter_opts_for_file(file, {formatter_opts, subs}) do + Enum.find_value(subs, formatter_opts, fn {sub, formatter_opts_and_subs} -> + size = byte_size(sub) + + case file do + <> + when prefix == sub and dir_separator in [?\\, ?/] -> + recur_formatter_opts_for_file(file, formatter_opts_and_subs) + + _ -> + nil + end + end) + end + + defp no_entries_in_formatter_opts?({formatter_opts, subs}) do + is_nil(formatter_opts[:inputs]) and subs == [] + end + + defp stdin_or_wildcard("-"), do: [:stdin] + + defp stdin_or_wildcard(path), + do: path |> Path.expand() |> Path.wildcard(match_dot: true) |> Enum.filter(&File.regular?/1) + + defp elixir_format(content, formatter_opts) do + case Code.format_string!(content, formatter_opts) do + [] -> "" + formatted_content -> IO.iodata_to_binary([formatted_content, ?\n]) + end + end + + defp find_sigils_from_plugins(plugin, formatter_opts) do + if Code.ensure_loaded?(plugin) and function_exported?(plugin, :features, 1) do + List.wrap(plugin.features(formatter_opts)[:sigils]) + else + [] + end + end + + defp read_file(:stdin), do: IO.stream() |> Enum.to_list() |> IO.iodata_to_binary() + defp read_file(file), do: File.read!(file) + + defp format_file({file, formatter}, task_opts) do + input = read_file(file) + output = formatter.(input) + check_formatted? = Keyword.get(task_opts, :check_formatted, false) + dry_run? = Keyword.get(task_opts, :dry_run, false) + + cond do + check_formatted? -> + if input == output, do: :ok, else: {:not_formatted, {file, input, output}} + + dry_run? -> + :ok + + true -> + write_or_print(file, input, output) + end + rescue + exception -> + {:exit, file, exception, __STACKTRACE__} + end + + defp write_or_print(file, input, output) do + cond do + file == :stdin -> IO.write(output) + input == output -> :ok + true -> File.write!(file, output) + end + + :ok + end + + defp collect_status({:ok, :ok}, acc), do: acc + + defp collect_status({:ok, {:exit, _, _, _} = exit}, {exits, not_formatted}) do + {[exit | exits], not_formatted} + end + + defp collect_status({:ok, {:not_formatted, file}}, {exits, not_formatted}) do + {exits, [file | not_formatted]} + end + + defp check!({[], []}, _task_opts) do + :ok + end + + defp check!({[{:exit, :stdin, exception, stacktrace} | _], _not_formatted}, _task_opts) do + Mix.shell().error("mix format failed for stdin") + reraise exception, stacktrace + end + + defp check!({[{:exit, file, exception, stacktrace} | _], _not_formatted}, _task_opts) do + Mix.shell().error("mix format failed for file: #{Path.relative_to_cwd(file)}") + reraise exception, stacktrace + end + + defp check!({_exits, [_ | _] = not_formatted}, task_opts) do + no_exit? = Keyword.get(task_opts, :no_exit, false) + + message = """ + The following files are not formatted: + + #{to_diffs(not_formatted)} + """ + + if no_exit? do + Mix.shell().info(message) + else + Mix.raise(""" + mix format failed due to --check-formatted. + #{message} + """) + end + end + + defp to_diffs(files) do + Enum.map_intersperse(files, "\n", fn + {:stdin, unformatted, formatted} -> + [IO.ANSI.reset(), text_diff_format(unformatted, formatted)] + + {file, unformatted, formatted} -> + [ + IO.ANSI.bright(), + IO.ANSI.red(), + file, + "\n", + IO.ANSI.reset(), + "\n", + text_diff_format(unformatted, formatted) + ] + end) + end + + @doc false + @spec text_diff_format(String.t(), String.t()) :: iolist() + def text_diff_format(old, new, opts \\ []) + + def text_diff_format(code, code, _opts), do: [] + + def text_diff_format(old, new, opts) do + opts = Keyword.validate!(opts, after: 2, before: 2, color: IO.ANSI.enabled?(), line: 1) + crs? = String.contains?(old, "\r") || String.contains?(new, "\r") + + old = String.split(old, "\n") + new = String.split(new, "\n") + + max = max(length(new), length(old)) + line_num_digits = max |> Integer.digits() |> length() + opts = Keyword.put(opts, :line_num_digits, line_num_digits) + + {line, opts} = Keyword.pop!(opts, :line) + + old + |> List.myers_difference(new) + |> insert_cr_symbols(crs?) + |> diff_to_iodata({line, line}, opts) + end + + defp diff_to_iodata(diff, line_nums, opts, iodata \\ []) + + defp diff_to_iodata([], _line_nums, _opts, iodata), do: Enum.reverse(iodata) + + defp diff_to_iodata([{:eq, [""]}], _line_nums, _opts, iodata), do: Enum.reverse(iodata) + + defp diff_to_iodata([{:eq, lines}], line_nums, opts, iodata) do + lines_after = Enum.take(lines, opts[:after]) + iodata = lines(iodata, {:eq, lines_after}, line_nums, opts) + + iodata = + if length(lines) > opts[:after] do + lines(iodata, :skip, opts) + else + iodata + end + + Enum.reverse(iodata) + end + + defp diff_to_iodata([{:eq, lines} | diff], {line, line}, opts, [] = iodata) do + {start, lines_before} = Enum.split(lines, opts[:before] * -1) + + iodata = + if length(lines) > opts[:before] do + lines(iodata, :skip, opts) + else + iodata + end + + line = line + length(start) + iodata = lines(iodata, {:eq, lines_before}, {line, line}, opts) + + line = line + length(lines_before) + diff_to_iodata(diff, {line, line}, opts, iodata) + end + + defp diff_to_iodata([{:eq, lines} | diff], line_nums, opts, iodata) do + if length(lines) > opts[:after] + opts[:before] do + {lines1, lines2, lines3} = split(lines, opts[:after], opts[:before] * -1) + + iodata = + iodata + |> lines({:eq, lines1}, line_nums, opts) + |> lines(:skip, opts) + |> lines({:eq, lines3}, add_line_nums(line_nums, length(lines1) + length(lines2)), opts) + + line_nums = add_line_nums(line_nums, length(lines)) + + diff_to_iodata(diff, line_nums, opts, iodata) + else + iodata = lines(iodata, {:eq, lines}, line_nums, opts) + line_nums = add_line_nums(line_nums, length(lines)) + + diff_to_iodata(diff, line_nums, opts, iodata) + end + end + + defp diff_to_iodata([{:del, [del]}, {:ins, [ins]} | diff], line_nums, opts, iodata) do + iodata = lines(iodata, {:chg, del, ins}, line_nums, opts) + diff_to_iodata(diff, add_line_nums(line_nums, 1), opts, iodata) + end + + defp diff_to_iodata([{kind, lines} | diff], line_nums, opts, iodata) do + iodata = lines(iodata, {kind, lines}, line_nums, opts) + line_nums = add_line_nums(line_nums, length(lines), kind) + + diff_to_iodata(diff, line_nums, opts, iodata) + end + + defp split(list, count1, count2) do + {split1, split2} = Enum.split(list, count1) + {split2, split3} = Enum.split(split2, count2) + {split1, split2, split3} + end + + defp lines(iodata, :skip, opts) do + line_num = String.duplicate(@blank, opts[:line_num_digits] * 2 + 1) + [[line_num, @gutter[:skip], @separator, @newline] | iodata] + end + + defp lines(iodata, {:chg, del, ins}, line_nums, opts) do + {del, ins} = line_diff(del, ins, opts) + + [ + [gutter(line_nums, :ins, opts), ins, @newline], + [gutter(line_nums, :del, opts), del, @newline] + | iodata + ] + end + + defp lines(iodata, {kind, lines}, line_nums, opts) do + lines + |> Enum.with_index() + |> Enum.reduce(iodata, fn {line, offset}, iodata -> + line_nums = add_line_nums(line_nums, offset, kind) + [[gutter(line_nums, kind, opts), colorize(line, kind, false, opts), @newline] | iodata] + end) + end + + defp gutter(line_nums, kind, opts) do + [line_num(line_nums, kind, opts), colorize(@gutter[kind], kind, false, opts), @separator] + end + + defp line_num({line_num_old, line_num_new}, :eq, opts) do + old = + line_num_old + |> to_string() + |> String.pad_leading(opts[:line_num_digits], @line_num_pad) + + new = + line_num_new + |> to_string() + |> String.pad_leading(opts[:line_num_digits], @line_num_pad) + + [old, @blank, new] + end + + defp line_num({line_num_old, _line_num_new}, :del, opts) do + old = + line_num_old + |> to_string() + |> String.pad_leading(opts[:line_num_digits], @line_num_pad) + + new = String.duplicate(@blank, opts[:line_num_digits]) + [old, @blank, new] + end + + defp line_num({_line_num_old, line_num_new}, :ins, opts) do + old = String.duplicate(@blank, opts[:line_num_digits]) + + new = + line_num_new + |> to_string() + |> String.pad_leading(opts[:line_num_digits], @line_num_pad) + + [old, @blank, new] + end + + defp line_diff(del, ins, opts) do + diff = String.myers_difference(del, ins) + + Enum.reduce(diff, {[], []}, fn + {:eq, str}, {del, ins} -> {[del | str], [ins | str]} + {:del, str}, {del, ins} -> {[del | colorize(str, :del, true, opts)], ins} + {:ins, str}, {del, ins} -> {del, [ins | colorize(str, :ins, true, opts)]} + end) + end + + defp colorize(str, kind, space?, opts) do + if Keyword.fetch!(opts, :color) && Keyword.has_key?(@colors, kind) do + color = Keyword.fetch!(@colors, kind) + + if space? do + str + |> String.split(~r/[\t\s]+/, include_captures: true) + |> Enum.map(fn + <> = str when start in ["\t", "\s"] -> + IO.ANSI.format([color[:space], str]) + + str -> + IO.ANSI.format([color[:text], str]) + end) + else + IO.ANSI.format([color[:text], str]) + end + else + str + end + end + + defp add_line_nums({line_num_old, line_num_new}, lines, kind \\ :eq) do + case kind do + :eq -> {line_num_old + lines, line_num_new + lines} + :ins -> {line_num_old, line_num_new + lines} + :del -> {line_num_old + lines, line_num_new} + end + end + + defp insert_cr_symbols(diffs, false), do: diffs + defp insert_cr_symbols(diffs, true), do: do_insert_cr_symbols(diffs, []) + + defp do_insert_cr_symbols([], acc), do: Enum.reverse(acc) + + defp do_insert_cr_symbols([{:del, del}, {:ins, ins} | rest], acc) do + {del, ins} = do_insert_cr_symbols(del, ins, {[], []}) + do_insert_cr_symbols(rest, [{:ins, ins}, {:del, del} | acc]) + end + + defp do_insert_cr_symbols([diff | rest], acc) do + do_insert_cr_symbols(rest, [diff | acc]) + end + + defp do_insert_cr_symbols([left | left_rest], [right | right_rest], {left_acc, right_acc}) do + {left, right} = insert_cr_symbol(left, right) + do_insert_cr_symbols(left_rest, right_rest, {[left | left_acc], [right | right_acc]}) + end + + defp do_insert_cr_symbols([], right, {left_acc, right_acc}) do + left = Enum.reverse(left_acc) + right = Enum.reverse(right_acc, right) + {left, right} + end + + defp do_insert_cr_symbols(left, [], {left_acc, right_acc}) do + left = Enum.reverse(left_acc, left) + right = Enum.reverse(right_acc) + {left, right} + end + + defp insert_cr_symbol(left, right) do + case {String.ends_with?(left, "\r"), String.ends_with?(right, "\r")} do + {bool, bool} -> {left, right} + {true, false} -> {String.replace(left, "\r", @cr), right} + {false, true} -> {left, String.replace(right, "\r", @cr)} + end + end +end + +defmodule :_next_ls_private_compiler_worker do + use GenServer + + def start_link(arg) do + GenServer.start_link(__MODULE__, arg, name: __MODULE__) + end + + @impl GenServer + def init(_arg) do + working_dir = File.cwd!() + {:ok, %{working_dir: working_dir}} + end + + def enqueue_compiler(opts) do + GenServer.cast(__MODULE__, {:compile, opts}) + end + + defp flush(acc) do + receive do + {:"$gen_cast", {:compile, opts}} -> flush([opts | acc]) + after + 0 -> acc + end + end + + @impl GenServer + def handle_cast({:compile, opts}, state) do + # we essentially compile now and rollup any newer requests to compile, so that we aren't doing 5 compiles + # if we the user saves 5 times after saving one time + newer_opts = flush([]) + from = Keyword.fetch!(opts, :from) + caller_ref = Keyword.fetch!(opts, :caller_ref) + + for opt <- newer_opts do + Process.send(opt[:from], {:compiler_canceled, opt[:caller_ref]}, []) + end + + File.cd!(state.working_dir) + + if opts[:force] do + File.rm_rf!(Path.join(opts[:working_dir], ".elixir-tools/_build")) + end + + result = :_next_ls_private_compiler.compile() + + Process.send(from, {:compiler_result, caller_ref, result}, []) + {:noreply, state} + end +end + defmodule :_next_ls_private_compiler do @moduledoc false + def start do + children = [ + :_next_ls_private_compiler_worker + ] + + # See https://hexdocs.pm/elixir/Supervisor.html + # for other strategies and supported options + opts = [strategy: :one_for_one, name: :_next_ls_private_application_supervisor] + {:ok, pid} = Supervisor.start_link(children, opts) + Process.unlink(pid) + {:ok, pid} + end + @tracers Code.get_compiler_option(:tracers) def compile do diff --git a/test/next_ls/autocomplete_test.exs b/test/next_ls/autocomplete_test.exs index 2736f3df..79ec8410 100644 --- a/test/next_ls/autocomplete_test.exs +++ b/test/next_ls/autocomplete_test.exs @@ -65,6 +65,7 @@ defmodule NextLS.AutocompleteTest do working_dir: cwd, uri: "file://#{cwd}", parent: self(), + lsp_pid: self(), logger: logger, db: :some_db, mix_env: "dev", @@ -76,7 +77,8 @@ defmodule NextLS.AutocompleteTest do assert_receive :ready - Runtime.compile(pid) + Runtime.compile(pid, caller_ref: make_ref()) + assert_receive {:compiler_result, _, _, {:ok, _}} [runtime: pid] end diff --git a/test/next_ls/dependency_test.exs b/test/next_ls/dependency_test.exs index be345faa..f7752fd5 100644 --- a/test/next_ls/dependency_test.exs +++ b/test/next_ls/dependency_test.exs @@ -1,5 +1,6 @@ defmodule NextLS.DependencyTest do - use ExUnit.Case, async: true + # FIXME: make async: true + use ExUnit.Case, async: false import GenLSP.Test import NextLS.Support.Utils diff --git a/test/next_ls/extensions/credo_extension_test.exs b/test/next_ls/extensions/credo_extension_test.exs index 441a4c3f..c01a20d0 100644 --- a/test/next_ls/extensions/credo_extension_test.exs +++ b/test/next_ls/extensions/credo_extension_test.exs @@ -2,7 +2,8 @@ defmodule NextLS.CredoExtensionTest do # this test installs and compiles credo from scratch everytime it runs # we need to determine a way to cache this without losing the utility of # the test. - use ExUnit.Case, async: true + # FIXME: make async: true + use ExUnit.Case, async: false import GenLSP.Test import NextLS.Support.Utils diff --git a/test/next_ls/runtime_test.exs b/test/next_ls/runtime_test.exs index 310e6817..caa763cd 100644 --- a/test/next_ls/runtime_test.exs +++ b/test/next_ls/runtime_test.exs @@ -129,6 +129,7 @@ defmodule NextLs.RuntimeTest do working_dir: cwd, uri: "file://#{cwd}", parent: self(), + lsp_pid: self(), logger: logger, db: :some_db, mix_env: "dev", @@ -157,6 +158,7 @@ defmodule NextLs.RuntimeTest do working_dir: cwd, uri: "file://#{cwd}", parent: self(), + lsp_pid: self(), logger: logger, db: :some_db, mix_env: "dev", @@ -186,6 +188,7 @@ defmodule NextLs.RuntimeTest do working_dir: cwd, uri: "file://#{cwd}", parent: self(), + lsp_pid: self(), logger: logger, db: :some_db, mix_env: "dev", @@ -193,21 +196,26 @@ defmodule NextLs.RuntimeTest do registry: RuntimeTest.Registry} ) - assert_receive :ready + assert_receive :ready, 5000 file = Path.join(cwd, "lib/bar.ex") - assert [ - %Mix.Task.Compiler.Diagnostic{ - file: ^file, - severity: :warning, - message: - "variable \"arg1\" is unused (if the variable is not meant to be used, prefix it with an underscore)", - position: position, - compiler_name: "Elixir", - details: nil - } - ] = Runtime.compile(pid) + ref = make_ref() + assert :ok == Runtime.compile(pid, caller_ref: ref) + + assert_receive {:compiler_result, ^ref, "my_proj", + {:ok, + [ + %Mix.Task.Compiler.Diagnostic{ + file: ^file, + severity: :warning, + message: + "variable \"arg1\" is unused (if the variable is not meant to be used, prefix it with an underscore)", + position: position, + compiler_name: "Elixir", + details: nil + } + ]}} if Version.match?(System.version(), ">= 1.15.0") do assert position == {4, 11} @@ -223,7 +231,10 @@ defmodule NextLs.RuntimeTest do end """) - assert [] == Runtime.compile(pid) + ref = make_ref() + assert :ok == Runtime.compile(pid, caller_ref: ref) + + assert_receive {:compiler_result, ^ref, "my_proj", {:ok, []}} end test "responds with an error when the runtime isn't ready", %{logger: logger, cwd: cwd, on_init: on_init} do @@ -240,6 +251,7 @@ defmodule NextLs.RuntimeTest do working_dir: cwd, uri: "file://#{cwd}", parent: self(), + lsp_pid: self(), logger: logger, db: :some_db, mix_env: "dev",