diff --git a/README.md b/README.md index 2e4baf21c..684f268d7 100644 --- a/README.md +++ b/README.md @@ -334,7 +334,6 @@ Below is a list of configuration options supported by the ElixirLS Debugger. Con
stackTraceMode
Debugger stacktrace mode - Allowed values are `all`, `no_tail`, and `false`.
requireFiles
A list of additional files that should be required and interpreted - This is especially useful for debugging tests.
debugInterpretModulesPatterns
A list of globs specifying modules that should be interpreted
-
debugExpressionTimeoutMs
Expression evaluator timeout in milliseconds - This defaults to 10 000.
projectDir
An absolute path to the directory where `mix.exs` is located - In VSCode, `${workspaceRoot}` can be used.
excludeModules
A list of modules that should not be interpreted
diff --git a/apps/elixir_ls_debugger/lib/debugger/protocol.basic.ex b/apps/elixir_ls_debugger/lib/debugger/protocol.basic.ex index 28e568ac3..ab4383b2c 100644 --- a/apps/elixir_ls_debugger/lib/debugger/protocol.basic.ex +++ b/apps/elixir_ls_debugger/lib/debugger/protocol.basic.ex @@ -2,7 +2,7 @@ defmodule ElixirLS.Debugger.Protocol.Basic do @moduledoc """ Macros for VS Code debug protocol messages - These macros can be used for pattern matching or for creating messages corresponding to the + These macros can be used for pattern matching or for creating messages corresponding to the request, response, and event types as specified in VS Code debug protocol. """ @@ -49,7 +49,9 @@ defmodule ElixirLS.Debugger.Protocol.Basic do "error" => %{ "id" => unquote(seq), "format" => unquote(format), - "variables" => unquote(variables) + "variables" => unquote(variables), + "showUser" => false, + "sendTelemetry" => false } } } diff --git a/apps/elixir_ls_debugger/lib/debugger/protocol.ex b/apps/elixir_ls_debugger/lib/debugger/protocol.ex index fb23e916d..906bce968 100644 --- a/apps/elixir_ls_debugger/lib/debugger/protocol.ex +++ b/apps/elixir_ls_debugger/lib/debugger/protocol.ex @@ -20,6 +20,12 @@ defmodule ElixirLS.Debugger.Protocol do end end + defmacro cancel_req(seq, args) do + quote do + request(unquote(seq), "cancel", unquote(args)) + end + end + defmacro launch_req(seq, config) do quote do request(unquote(seq), "launch", unquote(config)) diff --git a/apps/elixir_ls_debugger/lib/debugger/server.ex b/apps/elixir_ls_debugger/lib/debugger/server.ex index e9e439f6d..1d3740670 100644 --- a/apps/elixir_ls_debugger/lib/debugger/server.ex +++ b/apps/elixir_ls_debugger/lib/debugger/server.ex @@ -47,6 +47,8 @@ defmodule ElixirLS.Debugger.Server do vars_to_var_ids: %{} } }, + requests: %{}, + progresses: MapSet.new(), next_id: 1, output: Output, breakpoints: %{}, @@ -261,13 +263,47 @@ defmodule ElixirLS.Debugger.Server do {:noreply, %{state | dbg_session: from}} end + def handle_call({:request_finished, packet, result}, _from, state = %__MODULE__{}) do + if MapSet.member?(state.progresses, packet["seq"]) do + Output.send_event("progressEnd", %{ + "progressId" => packet["seq"] + }) + end + + case result do + {:error, e = %ServerError{}} -> + Output.send_error_response(packet, e.message, e.format, e.variables) + + {:ok, response_body} -> + Output.send_response(packet, response_body) + end + + state = %{ + state + | requests: Map.delete(state.requests, packet["seq"]), + progresses: MapSet.delete(state.progresses, packet["seq"]) + } + + {:reply, :ok, state} + end + + def handle_call( + {:get_variable_reference, child_type, pid, value}, + _from, + state = %__MODULE__{} + ) do + {state, var_id} = get_variable_reference(child_type, state, pid, value) + + {:reply, var_id, state} + end + @impl GenServer def handle_cast({:receive_packet, request(_, "disconnect") = packet}, state = %__MODULE__{}) do Output.send_response(packet, %{}) {:noreply, state, {:continue, :disconnect}} end - def handle_cast({:receive_packet, request(_, _) = packet}, state = %__MODULE__{}) do + def handle_cast({:receive_packet, request(seq, _) = packet}, state = %__MODULE__{}) do try do if state.client_info == nil do case packet do @@ -285,14 +321,30 @@ defmodule ElixirLS.Debugger.Server do } end else - {response_body, state} = handle_request(packet, state) - Output.send_response(packet, response_body) + state = + case handle_request(packet, state) do + {response_body, state} -> + Output.send_response(packet, response_body) + state + + {:async, fun, state} -> + {pid, _ref} = handle_request_async(packet, fun) + %{state | requests: Map.put(state.requests, seq, {pid, packet})} + end + {:noreply, state} end rescue e in ServerError -> Output.send_error_response(packet, e.message, e.format, e.variables) {:noreply, state} + catch + kind, error -> + {payload, stacktrace} = Exception.blame(kind, error, __STACKTRACE__) + message = Exception.format(kind, payload, stacktrace) + Output.debugger_console(message) + Output.send_error_response(packet, "internalServerError", message, %{}) + {:noreply, state} end end @@ -343,11 +395,47 @@ defmodule ElixirLS.Debugger.Server do end def handle_info({:DOWN, _ref, :process, pid, reason}, state = %__MODULE__{}) do - Output.debugger_important( - "debugged process #{inspect(pid)} exited with reason #{Exception.format_exit(reason)}" - ) - + paused_processes_count_before = map_size(state.paused_processes) state = handle_process_exit(state, pid) + paused_processes_count_after = map_size(state.paused_processes) + + if paused_processes_count_after < paused_processes_count_before do + Output.debugger_important( + "Paused process #{inspect(pid)} exited with reason #{Exception.format_exit(reason)}" + ) + end + + # if the exited process was a request handler respond with error + # and optionally end progress + request = + state.requests + |> Enum.find(fn {_request_id, {request_pid, _packet}} -> request_pid == pid end) + + state = + case request do + {request_id, {_pid, packet}} -> + Output.send_error_response( + packet, + "internalServerError", + "Request handler exited with reason #{Exception.format_exit(reason)}", + %{} + ) + + if MapSet.member?(state.progresses, request_id) do + Output.send_event("progressEnd", %{ + "progressId" => request_id + }) + end + + %{ + state + | requests: Map.delete(state.requests, request_id), + progresses: MapSet.delete(state.progresses, request_id) + } + + nil -> + state + end {:noreply, state} end @@ -409,6 +497,40 @@ defmodule ElixirLS.Debugger.Server do } end + defp handle_request(cancel_req(_, args), %__MODULE__{requests: requests} = state) do + # in or case progressId is requestId so choose first not null + request_or_progress_id = args["requestId"] || args["progressId"] + + state = + case requests do + %{^request_or_progress_id => {pid, packet}} -> + Process.exit(pid, :cancelled) + Output.send_error_response(packet, "cancelled", "cancelled", %{}) + + # send progressEnd if cancelling a progress + if MapSet.member?(state.progresses, request_or_progress_id) do + Output.send_event("progressEnd", %{ + "progressId" => request_or_progress_id + }) + end + + %{ + state + | requests: Map.delete(requests, request_or_progress_id), + progresses: MapSet.delete(state.progresses, request_or_progress_id) + } + + _ -> + Output.debugger_console( + "Received cancel request for unknown requestId: #{inspect(request_or_progress_id)}\n" + ) + + state + end + + {%{}, state} + end + defp handle_request(launch_req(_, config) = args, state = %__MODULE__{}) do if args["arguments"]["noDebug"] == true do Output.debugger_important("launch with no debug is not supported") @@ -616,6 +738,13 @@ defmodule ElixirLS.Debugger.Server do if pid do :int.attach(pid, build_attach_mfa(:paused)) + else + raise ServerError, + message: "invalidArgument", + format: "threadId not found: {threadId}", + variables: %{ + "threadId" => inspect(thread_id) + } end {%{}, state} @@ -734,63 +863,47 @@ defmodule ElixirLS.Debugger.Server do request(_, "variables", %{"variablesReference" => var_id} = args), state = %__MODULE__{} ) do - {state, vars_json} = - case find_var(state.paused_processes, var_id) do - {pid, var} -> - variables(state, pid, var, args["start"], args["count"], args["filter"]) - - nil -> - raise ServerError, - message: "invalidArgument", - format: "variablesReference not found: {variablesReference}", - variables: %{ - "variablesReference" => inspect(var_id) - } - end + async_fn = fn -> + {pid, var} = find_var!(state.paused_processes, var_id) + vars_json = variables(state, pid, var, args["start"], args["count"], args["filter"]) + %{"variables" => vars_json} + end - {%{"variables" => vars_json}, state} + {:async, async_fn, state} end defp handle_request( - request(_cmd, "evaluate", %{"expression" => expr} = args), + request(seq, "evaluate", %{"expression" => expr} = args), state = %__MODULE__{} ) do - timeout = Map.get(state.config, "debugExpressionTimeoutMs", 10_000) - {binding, env_for_eval} = binding_and_env(state.paused_processes, args["frameId"]) + Output.send_event("progressStart", %{ + "progressId" => seq, + "title" => "Evaluating expression", + "message" => expr, + "requestId" => seq, + "cancellable" => true + }) - result = evaluate_code_expression(expr, binding, env_for_eval, timeout) + state = %{state | progresses: MapSet.put(state.progresses, seq)} - case result do - {:ok, value} -> - child_type = Variables.child_type(value) - {state, var_id} = get_variable_reference(child_type, state, :evaluator, value) + async_fn = fn -> + {binding, env_for_eval} = binding_and_env(state.paused_processes, args["frameId"]) + value = evaluate_code_expression(expr, binding, env_for_eval) - json = - %{ - "result" => inspect(value), - "variablesReference" => var_id - } - |> maybe_append_children_number(state.client_info, child_type, value) - |> maybe_append_variable_type(state.client_info, value) - - {json, state} - - other -> - result_string = - if args["context"] == "hover" do - # avoid displaying hover info when evaluation crashed - "" - else - inspect(other) - end - - json = %{ - "result" => result_string, - "variablesReference" => 0 - } + child_type = Variables.child_type(value) + # we need to call here as get_variable_reference modifies the state + var_id = + GenServer.call(__MODULE__, {:get_variable_reference, child_type, :evaluator, value}) - {json, state} + %{ + "result" => inspect(value), + "variablesReference" => var_id + } + |> maybe_append_children_number(state.client_info, child_type, value) + |> maybe_append_variable_type(state.client_info, value) end + + {:async, async_fn, state} end defp handle_request(continue_req(_, thread_id) = args, state = %__MODULE__{}) do @@ -868,44 +981,48 @@ defmodule ElixirLS.Debugger.Server do end defp handle_request(completions_req(_, text) = args, state = %__MODULE__{}) do - # assume that the position is 1-based - line = (args["arguments"]["line"] || 1) - 1 - column = (args["arguments"]["column"] || 1) - 1 + async_fn = fn -> + # assume that the position is 1-based + line = (args["arguments"]["line"] || 1) - 1 + column = (args["arguments"]["column"] || 1) - 1 + + # for simplicity take only text from the given line up to column + line = + text + |> String.split(["\r\n", "\n", "\r"]) + |> Enum.at(line) + + # It is measured in UTF-16 code units and the client capability + # `columnsStartAt1` determines whether it is 0- or 1-based. + column = Utils.dap_character_to_elixir(line, column) + prefix = String.slice(line, 0, column) + + {binding, _env_for_eval} = + binding_and_env(state.paused_processes, args["arguments"]["frameId"]) + + vars = + binding + |> Enum.map(fn {name, value} -> + %ElixirSense.Core.State.VarInfo{ + name: name, + type: ElixirSense.Core.Binding.from_var(value) + } + end) - # for simplicity take only text from the given line up to column - line = - text - |> String.split(["\r\n", "\n", "\r"]) - |> Enum.at(line) - - # It is measured in UTF-16 code units and the client capability - # `columnsStartAt1` determines whether it is 0- or 1-based. - column = Utils.dap_character_to_elixir(line, column) - prefix = String.slice(line, 0, column) - - {binding, _env_for_eval} = - binding_and_env(state.paused_processes, args["arguments"]["frameId"]) - - vars = - binding - |> Enum.map(fn {name, value} -> - %ElixirSense.Core.State.VarInfo{ - name: name, - type: ElixirSense.Core.Binding.from_var(value) - } - end) + env = %ElixirSense.Core.State.Env{vars: vars} + metadata = %ElixirSense.Core.Metadata{} - env = %ElixirSense.Core.State.Env{vars: vars} - metadata = %ElixirSense.Core.Metadata{} + results = + ElixirSense.Providers.Suggestion.Complete.complete(prefix, env, metadata, {1, 1}) + |> Enum.map(&ElixirLS.Debugger.Completions.map/1) - results = - ElixirSense.Providers.Suggestion.Complete.complete(prefix, env, metadata, {1, 1}) - |> Enum.map(&ElixirLS.Debugger.Completions.map/1) + %{"targets" => results} + end - {%{"targets" => results}, state} + {:async, async_fn, state} end - defp handle_request(request(_, command), _state = %__MODULE__{}) when is_binary(command) do + defp handle_request(request(_, command), %__MODULE__{}) when is_binary(command) do raise ServerError, message: "notSupported", format: "Debugger request {command} is currently not supported", @@ -986,16 +1103,16 @@ defmodule ElixirLS.Debugger.Server do defp variables(state = %__MODULE__{}, pid, var, start, count, filter) do var_child_type = Variables.child_type(var) - children = - if var_child_type == nil or (filter != nil and Atom.to_string(var_child_type) != filter) do - [] - else - Variables.children(var, start, count) - end - - Enum.reduce(children, {state, []}, fn {name, value}, {state = %__MODULE__{}, result} -> + if var_child_type == nil or (filter != nil and Atom.to_string(var_child_type) != filter) do + [] + else + Variables.children(var, start, count) + end + |> Enum.reduce([], fn {name, value}, acc -> child_type = Variables.child_type(value) - {state, var_id} = get_variable_reference(child_type, state, pid, value) + + var_id = + GenServer.call(__MODULE__, {:get_variable_reference, child_type, pid, value}) json = %{ @@ -1006,8 +1123,9 @@ defmodule ElixirLS.Debugger.Server do |> maybe_append_children_number(state.client_info, child_type, value) |> maybe_append_variable_type(state.client_info, value) - {state, result ++ [json]} + [json | acc] end) + |> Enum.reverse() end defp get_variable_reference(nil, state, _pid, _value), do: {state, 0} @@ -1027,30 +1145,19 @@ defmodule ElixirLS.Debugger.Server do defp maybe_append_variable_type(map, _, _value), do: map - defp evaluate_code_expression(expr, binding, env_or_opts, timeout) do - task = - Task.async(fn -> - receive do - :continue -> :ok - end - - try do - {term, _bindings} = Code.eval_string(expr, binding, env_or_opts) - term - catch - error -> error - end - end) - - Process.unlink(task.pid) - send(task.pid, :continue) - - result = Task.yield(task, timeout) || Task.shutdown(task) + defp evaluate_code_expression(expr, binding, env_or_opts) do + try do + {term, _bindings} = Code.eval_string(expr, binding, env_or_opts) + term + catch + kind, error -> + {payload, stacktrace} = Exception.blame(kind, error, prune_stacktrace(__STACKTRACE__)) + message = Exception.format(kind, payload, stacktrace) - case result do - {:ok, data} -> {:ok, data} - nil -> :elixir_ls_expression_timeout - _otherwise -> result + reraise( + %ServerError{message: "evaluateError", format: message, variables: %{}}, + stacktrace + ) end end @@ -1097,19 +1204,35 @@ defmodule ElixirLS.Debugger.Server do ]} _ -> - Output.debugger_console("Unable to find frame #{inspect(frame_id)}") - {[], []} + raise ServerError, + message: "argumentError", + format: "Unable to find frame {frameId}", + variables: %{"frameId" => frame_id} end end - defp find_var(paused_processes, var_id) do - Enum.find_value(paused_processes, fn - {pid, %{var_ids_to_vars: %{^var_id => var}}} -> - {pid, var} + defp find_var!(paused_processes, var_id) do + result = + Enum.find_value(paused_processes, fn + {pid, %{var_ids_to_vars: %{^var_id => var}}} -> + {pid, var} - _ -> - nil - end) + _ -> + nil + end) + + case result do + nil -> + raise ServerError, + message: "invalidArgument", + format: "variablesReference not found: {variablesReference}", + variables: %{ + "variablesReference" => inspect(var_id) + } + + other -> + other + end end defp find_frame(paused_processes, frame_id) do @@ -1309,7 +1432,8 @@ defmodule ElixirLS.Debugger.Server do "supportsSingleThreadExecutionRequests" => true, "supportsEvaluateForHovers" => true, "supportsClipboardContext" => true, - "supportTerminateDebuggee" => false + "supportTerminateDebuggee" => false, + "supportsCancelRequest" => true } end @@ -1557,6 +1681,7 @@ defmodule ElixirLS.Debugger.Server do defp eval_hit_count(hit_count) do try do + # TODO binding? {term, _bindings} = Code.eval_string(hit_count, []) if is_integer(term) do @@ -1772,4 +1897,28 @@ defmodule ElixirLS.Debugger.Server do quote do: env end end + + defp handle_request_async(packet, func) do + parent = self() + + spawn_monitor(fn -> + result = + try do + {:ok, func.()} + rescue + e in ServerError -> + {:error, e} + catch + kind, error -> + {payload, stacktrace} = Exception.blame(kind, error, __STACKTRACE__) + message = Exception.format(kind, payload, stacktrace) + Output.debugger_console(message) + + {:error, + %ServerError{message: "internalServerError", format: message, variables: %{}}} + end + + GenServer.call(parent, {:request_finished, packet, result}, :infinity) + end) + end end diff --git a/apps/elixir_ls_debugger/test/debugger_test.exs b/apps/elixir_ls_debugger/test/debugger_test.exs index f8e53d597..c8fa30cd5 100644 --- a/apps/elixir_ls_debugger/test/debugger_test.exs +++ b/apps/elixir_ls_debugger/test/debugger_test.exs @@ -2908,91 +2908,203 @@ defmodule ElixirLS.Debugger.ServerTest do end describe "Watch section" do - defp gen_watch_expression_packet(expr) do + defp gen_watch_expression_packet(seq, expr) do %{ "arguments" => %{ "context" => "watch", "expression" => expr, - "frameId" => 123 + "frameId" => nil }, "command" => "evaluate", - "seq" => 1, + "seq" => seq, "type" => "request" } end - test "Evaluate expression with OK result", %{server: server} do + test "evaluate expression with OK result", %{server: server} do in_fixture(__DIR__, "mix_project", fn -> Server.receive_packet(server, initialize_req(1, %{})) assert_receive(response(_, 1, "initialize", _)) Server.receive_packet( server, - gen_watch_expression_packet("1 + 2 + 3 + 4") + gen_watch_expression_packet(1, "1 + 2 + 3 + 4") + ) + + assert_receive( + event(_, "progressStart", %{ + "cancellable" => true, + "message" => "1 + 2 + 3 + 4", + "progressId" => 1, + "requestId" => 1, + "title" => "Evaluating expression" + }) ) assert_receive(%{"body" => %{"result" => "10"}}, 1000) + assert_receive(event(_, "progressEnd", %{"progressId" => 1})) + assert Process.alive?(server) end) end @tag :capture_log - test "Evaluate expression with ERROR result", %{server: server} do + test "evaluate expression with exception result", %{server: server} do + in_fixture(__DIR__, "mix_project", fn -> + Server.receive_packet(server, initialize_req(1, %{})) + assert_receive(response(_, 1, "initialize", _)) + + Server.receive_packet( + server, + gen_watch_expression_packet(1, "1 = 2") + ) + + assert_receive( + event(_, "progressStart", %{ + "cancellable" => true, + "message" => "1 = 2", + "progressId" => 1, + "requestId" => 1, + "title" => "Evaluating expression" + }) + ) + + assert_receive( + error_response( + _, + 1, + "evaluate", + "evaluateError", + "** (MatchError) no match of right hand side value: 2" <> _, + %{} + ) + ) + + assert_receive(event(_, "progressEnd", %{"progressId" => 1})) + + assert Process.alive?(server) + end) + end + + test "evaluate expression which calls exit process", %{server: server} do in_fixture(__DIR__, "mix_project", fn -> Server.receive_packet(server, initialize_req(1, %{})) assert_receive(response(_, 1, "initialize", _)) Server.receive_packet( server, - gen_watch_expression_packet("1 = 2") + gen_watch_expression_packet(1, "exit(:normal)") ) - assert_receive(%{"body" => %{"result" => result}}, 1000) + assert_receive( + event(_, "progressStart", %{ + "cancellable" => true, + "message" => "exit(:normal)", + "progressId" => 1, + "requestId" => 1, + "title" => "Evaluating expression" + }) + ) - assert result =~ ~r/badmatch/ + # evaluator process exits so we should not get a response + refute_receive(%{"body" => %{"result" => _result}}, 1000) + + assert_receive( + error_response( + _, + 1, + "evaluate", + "evaluateError", + "** (exit) normal" <> _, + %{} + ) + ) + + assert_receive(event(_, "progressEnd", %{"progressId" => 1})) assert Process.alive?(server) end) end - test "Evaluate expression with attempt to exit debugger process", %{server: server} do + test "evaluate expression with attempt to exit debugger process", %{server: server} do in_fixture(__DIR__, "mix_project", fn -> Server.receive_packet(server, initialize_req(1, %{})) assert_receive(response(_, 1, "initialize", _)) Server.receive_packet( server, - gen_watch_expression_packet("Process.exit(self(), :normal)") + gen_watch_expression_packet(1, "Process.exit(self(), :normal)") ) - assert_receive(%{"body" => %{"result" => result}}, 1000) + assert_receive( + event(_, "progressStart", %{ + "cancellable" => true, + "message" => "Process.exit(self(), :normal)", + "progressId" => 1, + "requestId" => 1, + "title" => "Evaluating expression" + }) + ) - assert result =~ ~r/:exit/ + # evaluator process exits so we should not get a response + refute_receive(%{"body" => %{"result" => _result}}, 1000) + + assert_receive( + error_response( + _, + 1, + "evaluate", + "internalServerError", + "Request handler exited with reason normal", + %{} + ) + ) + + assert_receive(event(_, "progressEnd", %{"progressId" => 1})) assert Process.alive?(server) end) end - test "Evaluate expression with attempt to throw debugger process", %{server: server} do + test "evaluate expression with attempt to throw debugger process", %{server: server} do in_fixture(__DIR__, "mix_project", fn -> Server.receive_packet(server, initialize_req(1, %{})) assert_receive(response(_, 1, "initialize", _)) Server.receive_packet( server, - gen_watch_expression_packet("throw(:goodmorning_bug)") + gen_watch_expression_packet(1, "throw(:goodmorning_bug)") ) - assert_receive(%{"body" => %{"result" => result}}, 1000) + assert_receive( + event(_, "progressStart", %{ + "cancellable" => true, + "message" => "throw(:goodmorning_bug)", + "progressId" => 1, + "requestId" => 1, + "title" => "Evaluating expression" + }) + ) + + assert_receive( + error_response( + _, + 1, + "evaluate", + "evaluateError", + "** (throw) :goodmorning_bug" <> _, + %{} + ) + ) - assert result =~ ~r/:goodmorning_bug/ + assert_receive(event(_, "progressEnd", %{"progressId" => 1})) assert Process.alive?(server) end) end - test "Evaluate expression which has long execution", %{server: server} do + test "evaluate expression which has long execution", %{server: server} do in_fixture(__DIR__, "mix_project", fn -> Server.receive_packet(server, initialize_req(1, %{})) assert_receive(response(_, 1, "initialize", _)) @@ -3003,8 +3115,7 @@ defmodule ElixirLS.Debugger.ServerTest do "request" => "launch", "type" => "mix_task", "task" => "test", - "projectDir" => File.cwd!(), - "debugExpressionTimeoutMs" => 500 + "projectDir" => File.cwd!() }) ) @@ -3012,19 +3123,33 @@ defmodule ElixirLS.Debugger.ServerTest do Server.receive_packet( server, - gen_watch_expression_packet(":timer.sleep(10_000)") + gen_watch_expression_packet(1, ":timer.sleep(10_000)") ) - assert_receive(%{"body" => %{"result" => result}}, 1100) + Server.receive_packet( + server, + cancel_req(2, %{"progressId" => 1}) + ) - assert result =~ ~r/:elixir_ls_expression_timeout/ + assert_receive(response(_, 2, "cancel", _)) + + assert_receive( + error_response( + _, + 1, + "evaluate", + "cancelled", + "cancelled", + %{} + ) + ) assert Process.alive?(server) end) end end - test "Completions", %{server: server} do + test "completions", %{server: server} do in_fixture(__DIR__, "mix_project", fn -> Server.receive_packet(server, initialize_req(1, %{})) assert_receive(response(_, 1, "initialize", _)) @@ -3047,4 +3172,60 @@ defmodule ElixirLS.Debugger.ServerTest do assert Process.alive?(server) end) end + + test "completions cancel", %{server: server} do + in_fixture(__DIR__, "mix_project", fn -> + Server.receive_packet(server, initialize_req(1, %{})) + assert_receive(response(_, 1, "initialize", _)) + + Server.receive_packet( + server, + %{ + "arguments" => %{ + "text" => "DateTi", + "column" => 7 + }, + "command" => "completions", + "seq" => 1, + "type" => "request" + } + ) + + Server.receive_packet( + server, + cancel_req(2, %{"requestId" => 1}) + ) + + assert_receive(response(_, 2, "cancel", _)) + + assert_receive( + error_response( + _, + 1, + "completions", + "cancelled", + "cancelled", + %{} + ) + ) + + assert Process.alive?(server) + end) + end + + test "cancel not existing request", %{server: server} do + in_fixture(__DIR__, "mix_project", fn -> + Server.receive_packet(server, initialize_req(1, %{})) + assert_receive(response(_, 1, "initialize", _)) + + Server.receive_packet( + server, + cancel_req(2, %{"requestId" => 1}) + ) + + assert_receive(response(_, 2, "cancel", _)) + + assert Process.alive?(server) + end) + end end