From 88157678605dd9b8e08cfb1aa54792c0ffc9e900 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Samson?= Date: Sat, 2 Sep 2023 08:13:15 +0200 Subject: [PATCH] Debugger cancel (#976) * implement cancel and progress notification in debugger add progress notification and cancel support remove debugExpressionTimeoutMs setting add unexpected error handling do not return evaluation errors as success responses make completions async and cancellable make evaluate async and cancellable raise on invalid arguments * disable sendTelemetry * make variables request async --- README.md | 1 - .../lib/debugger/protocol.basic.ex | 6 +- .../lib/debugger/protocol.ex | 6 + .../elixir_ls_debugger/lib/debugger/server.ex | 405 ++++++++++++------ .../elixir_ls_debugger/test/debugger_test.exs | 229 ++++++++-- 5 files changed, 492 insertions(+), 155 deletions(-) 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