From 642f44d9a40ce4ed914e189776ce1f58038407a9 Mon Sep 17 00:00:00 2001 From: Mitchell Hanberg Date: Wed, 14 Jun 2023 08:59:16 -0400 Subject: [PATCH] feat: basic lsp --- .dialyzer_ignore.exs | 3 + .formatter.exs | 20 +- .github/workflows/ci.yaml | 13 ++ bin/nextls | 16 ++ bin/start | 7 + lib/next_ls.ex | 239 +++++++++++++++++++++-- lib/next_ls/application.ex | 5 +- lib/next_ls/lsp_supervisor.ex | 45 +++++ lib/next_ls/runtime.ex | 142 ++++++++++++++ mix.exs | 3 +- priv/cmd | 22 +++ priv/monkey/_next_ls_private_compiler.ex | 22 +++ test/next_ls/runtime_test.exs | 47 +++++ test/next_ls_test.exs | 200 ++++++++++++++++++- test/support/project/.formatter.exs | 4 + test/support/project/.gitignore | 26 +++ test/support/project/README.md | 21 ++ test/support/project/lib/bar.ex | 2 + test/support/project/lib/code_action.ex | 9 + test/support/project/lib/foo.ex | 2 + test/support/project/lib/project.ex | 5 + test/support/project/mix.exs | 25 +++ test/support/project/mix.lock | 2 + test/test_helper.exs | 17 +- 24 files changed, 874 insertions(+), 23 deletions(-) create mode 100644 .dialyzer_ignore.exs create mode 100755 bin/nextls create mode 100755 bin/start create mode 100644 lib/next_ls/lsp_supervisor.ex create mode 100644 lib/next_ls/runtime.ex create mode 100755 priv/cmd create mode 100644 priv/monkey/_next_ls_private_compiler.ex create mode 100644 test/next_ls/runtime_test.exs create mode 100644 test/support/project/.formatter.exs create mode 100644 test/support/project/.gitignore create mode 100644 test/support/project/README.md create mode 100644 test/support/project/lib/bar.ex create mode 100644 test/support/project/lib/code_action.ex create mode 100644 test/support/project/lib/foo.ex create mode 100644 test/support/project/lib/project.ex create mode 100644 test/support/project/mix.exs create mode 100644 test/support/project/mix.lock diff --git a/.dialyzer_ignore.exs b/.dialyzer_ignore.exs new file mode 100644 index 00000000..09650548 --- /dev/null +++ b/.dialyzer_ignore.exs @@ -0,0 +1,3 @@ +[ + {"lib/next_ls/lsp_supervisor.ex", :exact_eq} +] diff --git a/.formatter.exs b/.formatter.exs index d2cda26e..f058d889 100644 --- a/.formatter.exs +++ b/.formatter.exs @@ -1,4 +1,20 @@ -# Used by "mix format" [ - inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] + locals_without_parens: [ + assert_result: 2, + assert_notification: 2, + assert_result: 3, + assert_notification: 3, + notify: 2, + request: 2 + ], + line_length: 120, + import_deps: [:gen_lsp], + inputs: [ + "{mix,.formatter}.exs", + "{config,lib,}/**/*.{ex,exs}", + "test/next_ls_test.exs", + "test/test_helper.exs", + "test/next_ls/**/*.{ex,exs}", + "priv/**/*.ex" + ] ] diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 5057c43b..5867a167 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -32,6 +32,19 @@ jobs: - name: Install Dependencies run: mix deps.get + - name: Start EPMD + run: epmd -daemon + + - name: Compile test project + env: + MIX_ENV: credolsp + run: (cd test/support/project && mix deps.get && mix compile) + + - name: Compile + env: + MIX_ENV: test + run: mix compile + - name: Run Tests run: mix test diff --git a/bin/nextls b/bin/nextls new file mode 100755 index 00000000..69dc32be --- /dev/null +++ b/bin/nextls @@ -0,0 +1,16 @@ +#!/usr/bin/env -S elixir --sname undefined + +System.no_halt(true) + +Logger.configure(level: :none) + +Mix.start() +Mix.shell(Mix.Shell.Process) + +default_version = "0.1.0" # x-release-please-version + +Mix.install([{:next_ls, System.get_env("NEXTLS_VERSION", default_version)}]) + +Logger.configure(level: :info) + +Application.ensure_all_started(:next_ls) diff --git a/bin/start b/bin/start new file mode 100755 index 00000000..7503ede5 --- /dev/null +++ b/bin/start @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +# used for local development + +cd "$(dirname "$0")"/.. || exit 1 + +elixir --sname undefined -S mix run --no-halt -e "Application.ensure_all_started(:next_ls)" -- "$@" diff --git a/lib/next_ls.ex b/lib/next_ls.ex index bc2f4c40..30ca5413 100644 --- a/lib/next_ls.ex +++ b/lib/next_ls.ex @@ -1,18 +1,235 @@ defmodule NextLS do - @moduledoc """ - Documentation for `NextLS`. - """ + @moduledoc false + use GenLSP - @doc """ - Hello world. + alias GenLSP.ErrorResponse - ## Examples + alias GenLSP.Enumerations.{ + ErrorCodes, + TextDocumentSyncKind + } - iex> NextLS.hello() - :world + alias GenLSP.Notifications.{ + Exit, + Initialized, + TextDocumentDidChange, + TextDocumentDidOpen, + TextDocumentDidSave + } - """ - def hello do - :world + alias GenLSP.Requests.{Initialize, Shutdown} + + alias GenLSP.Structures.{ + DidOpenTextDocumentParams, + InitializeParams, + InitializeResult, + SaveOptions, + ServerCapabilities, + TextDocumentItem, + TextDocumentSyncOptions + } + + def start_link(args) do + {args, opts} = Keyword.split(args, [:task_supervisor, :runtime_supervisor]) + + GenLSP.start_link(__MODULE__, args, opts) + end + + @impl true + def init(lsp, args) do + task_supervisor = Keyword.fetch!(args, :task_supervisor) + runtime_supervisor = Keyword.fetch!(args, :runtime_supervisor) + + {:ok, + assign(lsp, + exit_code: 1, + documents: %{}, + refresh_refs: %{}, + task_supervisor: task_supervisor, + runtime_supervisor: runtime_supervisor, + runtime_task: nil, + ready: false + )} + end + + @impl true + def handle_request( + %Initialize{params: %InitializeParams{root_uri: root_uri}}, + lsp + ) do + {:reply, + %InitializeResult{ + capabilities: %ServerCapabilities{ + text_document_sync: %TextDocumentSyncOptions{ + open_close: true, + save: %SaveOptions{include_text: true}, + change: TextDocumentSyncKind.full() + } + }, + server_info: %{name: "NextLS"} + }, assign(lsp, root_uri: root_uri)} + end + + def handle_request(%Shutdown{}, lsp) do + {:reply, nil, assign(lsp, exit_code: 0)} + end + + def handle_request(%{method: method}, lsp) do + GenLSP.warning(lsp, "[NextLS] Method Not Found: #{method}") + + {:reply, + %ErrorResponse{ + code: ErrorCodes.method_not_found(), + message: "Method Not Found: #{method}" + }, lsp} + end + + @impl true + def handle_notification(%Initialized{}, lsp) do + GenLSP.log(lsp, "[NextLS] LSP Initialized!") + + working_dir = URI.parse(lsp.assigns.root_uri).path + + GenLSP.log(lsp, "[NextLS] Booting runime...") + + {:ok, runtime} = + DynamicSupervisor.start_child( + lsp.assigns.runtime_supervisor, + {NextLS.Runtime, working_dir: working_dir, parent: self()} + ) + + Process.monitor(runtime) + + lsp = assign(lsp, runtime: runtime) + + task = + Task.Supervisor.async_nolink(lsp.assigns.task_supervisor, fn -> + with false <- + wait_until(fn -> + NextLS.Runtime.ready?(runtime) + end) do + GenLSP.error(lsp, "Failed to start runtime") + raise "Failed to boot runtime" + end + + GenLSP.log(lsp, "[NextLS] Runtime ready...") + + :ready + end) + + {:noreply, assign(lsp, runtime_task: task)} + end + + def handle_notification(%TextDocumentDidSave{}, %{assigns: %{ready: false}} = lsp) do + {:noreply, lsp} + end + + def handle_notification( + %TextDocumentDidSave{ + params: %GenLSP.Structures.DidSaveTextDocumentParams{ + text: text, + text_document: %{uri: uri} + } + }, + %{assigns: %{ready: true}} = lsp + ) do + {:noreply, lsp |> then(&put_in(&1.assigns.documents[uri], String.split(text, "\n")))} + end + + def handle_notification(%TextDocumentDidChange{}, %{assigns: %{ready: false}} = lsp) do + {:noreply, lsp} + end + + def handle_notification(%TextDocumentDidChange{}, lsp) do + for task <- Task.Supervisor.children(lsp.assigns.task_supervisor), + task != lsp.assigns.runtime_task do + Process.exit(task, :kill) + end + + {:noreply, lsp} + end + + def handle_notification( + %TextDocumentDidOpen{ + params: %DidOpenTextDocumentParams{ + text_document: %TextDocumentItem{text: text, uri: uri} + } + }, + lsp + ) do + {:noreply, put_in(lsp.assigns.documents[uri], String.split(text, "\n"))} + end + + def handle_notification(%Exit{}, lsp) do + System.halt(lsp.assigns.exit_code) + + {:noreply, lsp} + end + + def handle_notification(_notification, lsp) do + {:noreply, lsp} + end + + def handle_info({ref, resp}, %{assigns: %{refresh_refs: refs}} = lsp) + when is_map_key(refs, ref) do + Process.demonitor(ref, [:flush]) + {_token, refs} = Map.pop(refs, ref) + + lsp = + case resp do + :ready -> + assign(lsp, ready: true) + + _ -> + lsp + end + + {: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) + + {: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") + + {:noreply, assign(lsp, runtime: nil)} + end + + def handle_info({:log, message}, lsp) do + GenLSP.log(lsp, String.trim(message)) + + {:noreply, lsp} + end + + def handle_info(_, lsp) do + {:noreply, lsp} + end + + defp wait_until(cb) do + wait_until(120, cb) + end + + defp wait_until(0, _cb) do + false + end + + defp wait_until(n, cb) do + if cb.() do + true + else + Process.sleep(1000) + wait_until(n - 1, cb) + end end end diff --git a/lib/next_ls/application.ex b/lib/next_ls/application.ex index 9fd62b18..f16ed19f 100644 --- a/lib/next_ls/application.ex +++ b/lib/next_ls/application.ex @@ -7,10 +7,7 @@ defmodule NextLS.Application do @impl true def start(_type, _args) do - children = [ - # Starts a worker by calling: NextLS.Worker.start_link(arg) - # {NextLS.Worker, arg} - ] + children = [NextLS.LSPSupervisor] # See https://hexdocs.pm/elixir/Supervisor.html # for other strategies and supported options diff --git a/lib/next_ls/lsp_supervisor.ex b/lib/next_ls/lsp_supervisor.ex new file mode 100644 index 00000000..b48d194c --- /dev/null +++ b/lib/next_ls/lsp_supervisor.ex @@ -0,0 +1,45 @@ +defmodule NextLS.LSPSupervisor do + @moduledoc false + + use Supervisor + + @env Mix.env() + + def start_link(init_arg) do + Supervisor.start_link(__MODULE__, init_arg, name: __MODULE__) + end + + @impl true + def init(_init_arg) do + if @env == :test do + :ignore + else + {opts, _} = + OptionParser.parse!(System.argv(), + strict: [stdio: :boolean, port: :integer] + ) + + buffer_opts = + cond do + opts[:stdio] -> + [] + + is_integer(opts[:port]) -> + IO.puts("Starting on port #{opts[:port]}") + [communication: {GenLSP.Communication.TCP, [port: opts[:port]]}] + + true -> + raise "Unknown option" + end + + children = [ + {DynamicSupervisor, name: NextLS.RuntimeSupervisor}, + {Task.Supervisor, name: NextLS.TaskSupervisor}, + {GenLSP.Buffer, buffer_opts}, + {NextLS, task_supervisor: NextLS.TaskSupervisor, runtime_supervisor: NextLS.RuntimeSupervisor} + ] + + Supervisor.init(children, strategy: :one_for_one) + end + end +end diff --git a/lib/next_ls/runtime.ex b/lib/next_ls/runtime.ex new file mode 100644 index 00000000..410a446e --- /dev/null +++ b/lib/next_ls/runtime.ex @@ -0,0 +1,142 @@ +defmodule NextLS.Runtime do + @moduledoc false + use GenServer + + @exe :code.priv_dir(:next_ls) + |> Path.join("cmd") + |> Path.absname() + + def start_link(opts) do + GenServer.start_link(__MODULE__, opts, Keyword.take(opts, [:name])) + end + + @spec call(pid(), mfa()) :: any() + def call(server, mfa), do: GenServer.call(server, {:call, mfa}) + + @spec ready?(pid()) :: boolean() + def ready?(server), do: GenServer.call(server, :ready?) + + @spec await(pid(), non_neg_integer()) :: :ok | :timeout + def await(server, count \\ 50) + + def await(_server, 0) do + :timeout + end + + def await(server, count) do + if ready?(server) do + :ok + else + Process.sleep(500) + await(server, count - 1) + end + end + + @impl GenServer + def init(opts) do + sname = "nextls#{System.system_time()}" + working_dir = Keyword.fetch!(opts, :working_dir) + parent = Keyword.fetch!(opts, :parent) + + port = + Port.open( + {:spawn_executable, @exe}, + [ + :use_stdio, + :stderr_to_stdout, + :binary, + :stream, + cd: working_dir, + env: [ + {'MIX_ENV', 'dev'}, + {'MIX_BUILD_ROOT', '.elixir-tools/_build'} + ], + args: [ + System.find_executable("elixir"), + "--sname", + sname, + "-S", + "mix", + "run", + "--no-halt", + "--no-compile", + "--no-start" + ] + ] + ) + + Port.monitor(port) + + me = self() + + Task.start_link(fn -> + with {:ok, host} <- :inet.gethostname(), + node <- :"#{sname}@#{host}", + true <- connect(node, port, 120) do + send(parent, {:log, "Connected to node #{node}"}) + + :next_ls + |> :code.priv_dir() + |> Path.join("monkey/_next_ls_private_compiler.ex") + |> then(&:rpc.call(node, Code, :compile_file, [&1])) + + :ok = + :rpc.call( + node, + :_next_ls_private_compiler, + :compile, + [] + ) + + send(me, {:node, node}) + else + _ -> send(me, :cancel) + end + end) + + {:ok, %{port: port, parent: parent}} + end + + @impl GenServer + def handle_call(:ready?, _from, %{node: _node} = state) do + {:reply, true, state} + end + + def handle_call(:ready?, _from, state) do + {:reply, false, state} + end + + def handle_call({:call, {m, f, a}}, _from, %{node: node} = state) do + reply = :rpc.call(node, m, f, a) + {:reply, reply, state} + end + + @impl GenServer + def handle_info({:node, node}, state) do + Node.monitor(node, true) + {:noreply, Map.put(state, :node, node)} + end + + def handle_info({port, {:data, data}}, %{port: port} = state) do + send(state.parent, {:log, data}) + {:noreply, state} + end + + def handle_info({port, other}, %{port: port} = state) do + send(state.parent, {:log, other}) + {:noreply, state} + end + + defp connect(_node, _port, 0) do + raise "failed to connect" + end + + defp connect(node, port, attempts) do + if Node.connect(node) in [false, :ignored] do + Process.sleep(1000) + connect(node, port, attempts - 1) + else + true + end + end +end diff --git a/mix.exs b/mix.exs index 042e15ff..3aed97b6 100644 --- a/mix.exs +++ b/mix.exs @@ -9,7 +9,8 @@ defmodule NextLS.MixProject do elixir: "~> 1.13", start_permanent: Mix.env() == :prod, package: package(), - deps: deps() + deps: deps(), + dialyzer: [ignore_warnings: ".dialyzer_ignore.exs"] ] end diff --git a/priv/cmd b/priv/cmd new file mode 100755 index 00000000..6121c23a --- /dev/null +++ b/priv/cmd @@ -0,0 +1,22 @@ +#!/usr/bin/env bash + +# Start the program in the background +exec "$@" & +pid1=$! + +# Silence warnings from here on +exec >/dev/null 2>&1 + +# Read from stdin in the background and +# kill running program when stdin closes +exec 0<&0 $( + while read; do :; done + kill -KILL $pid1 +) & +pid2=$! + +# Clean up +wait $pid1 +ret=$? +kill -KILL $pid2 +exit $ret diff --git a/priv/monkey/_next_ls_private_compiler.ex b/priv/monkey/_next_ls_private_compiler.ex new file mode 100644 index 00000000..ef291e83 --- /dev/null +++ b/priv/monkey/_next_ls_private_compiler.ex @@ -0,0 +1,22 @@ +defmodule :_next_ls_private_compiler do + @moduledoc false + + def compile() do + # keep stdout on this node + Process.group_leader(self(), Process.whereis(:user)) + + # load the paths for deps and compile them + # will noop if they are already compiled + # The mix cli basically runs this before any mix task + # we have to rerun because we already ran a mix task + # (mix run), which called this, but we also passed + # --no-compile, so nothing was compiled, but the + # task was not re-enabled it seems + Mix.Task.rerun("deps.loadpaths") + Mix.Task.rerun("compile") + + :ok + rescue + e -> {:error, e} + end +end diff --git a/test/next_ls/runtime_test.exs b/test/next_ls/runtime_test.exs new file mode 100644 index 00000000..84c19c3b --- /dev/null +++ b/test/next_ls/runtime_test.exs @@ -0,0 +1,47 @@ +defmodule NextLs.RuntimeTest do + use ExUnit.Case, async: true + + @moduletag :tmp_dir + + require Logger + import ExUnit.CaptureLog + + alias NextLS.Runtime + + setup %{tmp_dir: tmp_dir} do + File.cp_r!("test/support/project", tmp_dir) + + {:ok, logger} = + Task.start_link(fn -> + recv = fn recv -> + receive do + {:log, msg} -> + Logger.debug(msg) + end + + recv.(recv) + end + + recv.(recv) + end) + + [logger: logger, cwd: Path.absname(tmp_dir)] + end + + test "can run code on the node", %{logger: logger, cwd: cwd} do + capture_log(fn -> + pid = start_supervised!({Runtime, working_dir: cwd, parent: logger}) + + Process.link(pid) + + assert wait_for_ready(pid) + end) =~ "Connected to node" + end + + defp wait_for_ready(pid) do + with false <- Runtime.ready?(pid) do + Process.sleep(100) + wait_for_ready(pid) + end + end +end diff --git a/test/next_ls_test.exs b/test/next_ls_test.exs index 1670a047..b802766f 100644 --- a/test/next_ls_test.exs +++ b/test/next_ls_test.exs @@ -1,8 +1,200 @@ defmodule NextLSTest do - use ExUnit.Case - doctest NextLS + use ExUnit.Case, async: true - test "greets the world" do - assert NextLS.hello() == :world + @moduletag :tmp_dir + + import GenLSP.Test + + setup %{tmp_dir: tmp_dir} do + File.cp_r!("test/support/project", tmp_dir) + + root_path = Path.absname(tmp_dir) + + tvisor = start_supervised!(Task.Supervisor) + rvisor = start_supervised!({DynamicSupervisor, [strategy: :one_for_one]}) + + server = + server(NextLS, + task_supervisor: tvisor, + runtime_supervisor: rvisor + ) + + Process.link(server.lsp) + + client = client(server) + + assert :ok == + request(client, %{ + method: "initialize", + id: 1, + jsonrpc: "2.0", + params: %{capabilities: %{}, rootUri: "file://#{root_path}"} + }) + + [server: server, client: client, cwd: root_path] + end + + test "can start the LSP server", %{server: server} do + assert alive?(server) + end + + test "responds correctly to a shutdown request", %{client: client} do + assert :ok == + notify(client, %{ + method: "initialized", + jsonrpc: "2.0", + params: %{} + }) + + assert_notification( + "window/logMessage", + %{"message" => "[NextLS] Runtime ready..."} + ) + + assert :ok == + request(client, %{ + method: "shutdown", + id: 2, + jsonrpc: "2.0", + params: nil + }) + + assert_result(2, nil) + end + + test "returns method not found for unimplemented requests", %{ + client: client + } do + id = System.unique_integer([:positive]) + + assert :ok == + notify(client, %{ + method: "initialized", + jsonrpc: "2.0", + params: %{} + }) + + assert :ok == + request(client, %{ + method: "textDocument/documentSymbol", + id: id, + jsonrpc: "2.0", + params: %{ + textDocument: %{ + uri: "file://file/doesnt/matter.ex" + } + } + }) + + assert_notification( + "window/logMessage", + %{ + "message" => "[NextLS] Method Not Found: textDocument/documentSymbol", + "type" => 2 + } + ) + + assert_error( + ^id, + %{ + "code" => -32_601, + "message" => "Method Not Found: textDocument/documentSymbol" + } + ) + end + + test "can initialize the server" do + assert_result( + 1, + %{ + "capabilities" => %{ + "textDocumentSync" => %{ + "openClose" => true, + "save" => %{ + "includeText" => true + }, + "change" => 1 + } + }, + "serverInfo" => %{"name" => "NextLS"} + } + ) + end + + @tag :pending + test "publishes diagnostics once the client has initialized", %{ + client: _client, + cwd: _cwd + } do + # assert :ok == + # notify(client, %{ + # method: "initialized", + # jsonrpc: "2.0", + # params: %{} + # }) + + # assert_notification( + # "window/logMessage", + # %{ + # "message" => "[NextLS] LSP Initialized!", + # "type" => 4 + # } + # ) + + # assert_notification("$/progress", %{"value" => %{"kind" => "begin"}}) + + # for file <- ["foo.ex", "bar.ex"] do + # uri = + # to_string(%URI{ + # host: "", + # scheme: "file", + # path: Path.join([cwd, "lib", file]) + # }) + + # assert_notification( + # "textDocument/publishDiagnostics", + # %{ + # "uri" => ^uri, + # "diagnostics" => [ + # %{ + # "source" => "credo", + # "code" => "NextLS.Check.Readability.ModuleDoc", + # "codeDescription" => %{ + # "href" => "https://hexdocs.pm/credo/NextLS.Check.Readability.ModuleDoc.html" + # }, + # "severity" => 3 + # } + # ] + # } + # ) + # end + + # uri = + # to_string(%URI{ + # host: "", + # scheme: "file", + # path: Path.join([cwd, "lib", "code_action.ex"]) + # }) + + # assert_notification( + # "textDocument/publishDiagnostics", + # %{ + # "uri" => ^uri, + # "diagnostics" => [ + # %{"severity" => 3}, + # %{"severity" => 3} + # ] + # } + # ) + + # assert_notification( + # "$/progress", + # %{ + # "value" => %{ + # "kind" => "end", + # "message" => "Found 5 issues" + # } + # } + # ) end end diff --git a/test/support/project/.formatter.exs b/test/support/project/.formatter.exs new file mode 100644 index 00000000..d2cda26e --- /dev/null +++ b/test/support/project/.formatter.exs @@ -0,0 +1,4 @@ +# Used by "mix format" +[ + inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] +] diff --git a/test/support/project/.gitignore b/test/support/project/.gitignore new file mode 100644 index 00000000..89d8f868 --- /dev/null +++ b/test/support/project/.gitignore @@ -0,0 +1,26 @@ +# The directory Mix will write compiled artifacts to. +/_build/ + +# If you run "mix test --cover", coverage assets end up here. +/cover/ + +# The directory Mix downloads your dependencies sources to. +/deps/ + +# Where third-party dependencies like ExDoc output generated docs. +/doc/ + +# Ignore .fetch files in case you like to edit your project deps locally. +/.fetch + +# If the VM crashes, it generates a dump, let's ignore it too. +erl_crash.dump + +# Also ignore archive artifacts (built via "mix archive.build"). +*.ez + +# Ignore package tarball (built via "mix hex.build"). +project-*.tar + +# Temporary files, for example, from tests. +/tmp/ diff --git a/test/support/project/README.md b/test/support/project/README.md new file mode 100644 index 00000000..dcbbf7c5 --- /dev/null +++ b/test/support/project/README.md @@ -0,0 +1,21 @@ +# Project + +**TODO: Add description** + +## Installation + +If [available in Hex](https://hex.pm/docs/publish), the package can be installed +by adding `project` to your list of dependencies in `mix.exs`: + +```elixir +def deps do + [ + {:project, "~> 0.1.0"} + ] +end +``` + +Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc) +and published on [HexDocs](https://hexdocs.pm). Once published, the docs can +be found at . + diff --git a/test/support/project/lib/bar.ex b/test/support/project/lib/bar.ex new file mode 100644 index 00000000..cb2fd689 --- /dev/null +++ b/test/support/project/lib/bar.ex @@ -0,0 +1,2 @@ +defmodule Bar do +end diff --git a/test/support/project/lib/code_action.ex b/test/support/project/lib/code_action.ex new file mode 100644 index 00000000..7ad0ff63 --- /dev/null +++ b/test/support/project/lib/code_action.ex @@ -0,0 +1,9 @@ +defmodule Foo.CodeAction do + # some comment + + defmodule NestedMod do + def foo do + :ok + end + end +end diff --git a/test/support/project/lib/foo.ex b/test/support/project/lib/foo.ex new file mode 100644 index 00000000..b921e00e --- /dev/null +++ b/test/support/project/lib/foo.ex @@ -0,0 +1,2 @@ +defmodule Foo do +end diff --git a/test/support/project/lib/project.ex b/test/support/project/lib/project.ex new file mode 100644 index 00000000..2d35018f --- /dev/null +++ b/test/support/project/lib/project.ex @@ -0,0 +1,5 @@ +defmodule Project do + def hello do + :world + end +end diff --git a/test/support/project/mix.exs b/test/support/project/mix.exs new file mode 100644 index 00000000..be61cf23 --- /dev/null +++ b/test/support/project/mix.exs @@ -0,0 +1,25 @@ +defmodule Project.MixProject do + use Mix.Project + + def project do + [ + app: :project, + version: "0.1.0", + elixir: "~> 1.10", + start_permanent: Mix.env() == :prod, + deps: deps() + ] + end + + # Run "mix help compile.app" to learn about applications. + def application do + [ + extra_applications: [:logger] + ] + end + + # Run "mix help deps" to learn about dependencies. + defp deps do + [] + end +end diff --git a/test/support/project/mix.lock b/test/support/project/mix.lock new file mode 100644 index 00000000..0ac823b3 --- /dev/null +++ b/test/support/project/mix.lock @@ -0,0 +1,2 @@ +%{ +} diff --git a/test/test_helper.exs b/test/test_helper.exs index 869559e7..fc000cd3 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -1 +1,16 @@ -ExUnit.start() +{:ok, _pid} = Node.start(:"nextls#{System.system_time()}", :shortnames) + +Logger.configure(level: :warn) + +timeout = + if System.get_env("CI", "false") == "true" do + 60_000 + else + 30_000 + end + +ExUnit.start( + exclude: [pending: true], + assert_receive_timeout: timeout, + timeout: 120_000 +)