From 3e531b92524c88b90a1fb2bec5f3e5dd9807af15 Mon Sep 17 00:00:00 2001 From: Fenix Date: Fri, 11 Feb 2022 19:11:06 +0800 Subject: [PATCH 001/125] Fix hover bug (#674) * #673 fix hover bug for * use function_exported to check module be loaded Change-Id: I8e3c3cde067a7d3313422c0a94f4c4a464bd37de * remove rescue and use if instead of nested cond Change-Id: Ia85056971d7333df5d6a7cf6d3a6bd21c87635eb Co-authored-by: zhuzhenfeng.code --- .../lib/language_server/providers/hover.ex | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/apps/language_server/lib/language_server/providers/hover.ex b/apps/language_server/lib/language_server/providers/hover.ex index 53d295aad..1c61e8ab1 100644 --- a/apps/language_server/lib/language_server/providers/hover.ex +++ b/apps/language_server/lib/language_server/providers/hover.ex @@ -131,12 +131,15 @@ defmodule ElixirLS.LanguageServer.Providers.Hover do end defp dep_name(root_mod_name, project_dir) do - s = root_mod_name |> source() - - cond do - third_dep?(s, project_dir) -> third_dep_name(s, project_dir) - builtin?(s) -> builtin_dep_name(s) - true -> "" + if not elixir_mod_exported?(root_mod_name) do + "" + else + s = root_mod_name |> source() + cond do + third_dep?(s, project_dir) -> third_dep_name(s, project_dir) + builtin?(s) -> builtin_dep_name(s) + true -> "" + end end end @@ -149,6 +152,10 @@ defmodule ElixirLS.LanguageServer.Providers.Hover do dep.__info__(:compile) |> Keyword.get(:source) |> List.to_string() end + defp elixir_mod_exported?(mod_name) do + ("Elixir." <> mod_name) |> String.to_atom() |> function_exported?(:__info__, 1) + end + defp third_dep?(source, project_dir) do prefix = project_dir <> "/deps" String.starts_with?(source, prefix) From c5fd13fed147997649433eb010992cbc3f28d03e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Samson?= Date: Sun, 13 Feb 2022 10:59:21 +0100 Subject: [PATCH 002/125] Add support for variable expansion of evaluate results (#672) compatibility improvement: only return variable type and pagination properties when supported by client run evaluate in a stacktrace frame if specified --- .../elixir_ls_debugger/lib/debugger/server.ex | 116 ++++++++++++------ .../elixir_ls_debugger/test/debugger_test.exs | 9 +- 2 files changed, 86 insertions(+), 39 deletions(-) diff --git a/apps/elixir_ls_debugger/lib/debugger/server.ex b/apps/elixir_ls_debugger/lib/debugger/server.ex index 2149371ff..125fd78e8 100644 --- a/apps/elixir_ls_debugger/lib/debugger/server.ex +++ b/apps/elixir_ls_debugger/lib/debugger/server.ex @@ -35,7 +35,12 @@ defmodule ElixirLS.Debugger.Server do task_ref: nil, threads: %{}, threads_inverse: %{}, - paused_processes: %{}, + paused_processes: %{ + evaluator: %{ + vars: %{}, + vars_inverse: %{} + } + }, next_id: 1, output: Output, breakpoints: %{}, @@ -499,15 +504,26 @@ defmodule ElixirLS.Debugger.Server do end defp handle_request( - request(_cmd, "evaluate", %{"expression" => expr} = _args), + request(_cmd, "evaluate", %{"expression" => expr} = args), state = %__MODULE__{} ) do timeout = Map.get(state.config, "debugExpressionTimeoutMs", 10_000) - bindings = all_variables(state.paused_processes) + bindings = all_variables(state.paused_processes, args["frameId"]) + + value = evaluate_code_expression(expr, bindings, timeout) - result = evaluate_code_expression(expr, bindings, timeout) + child_type = Variables.child_type(value) + {state, var_id} = get_variable_reference(child_type, state, :evaluator, value) + + 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) - {%{"result" => inspect(result), "variablesReference" => 0}, state} + {json, state} end defp handle_request(continue_req(_, thread_id), state = %__MODULE__{}) do @@ -623,34 +639,39 @@ defmodule ElixirLS.Debugger.Server do end Enum.reduce(children, {state, []}, fn {name, value}, {state = %__MODULE__{}, result} -> - num_children = Variables.num_children(value) child_type = Variables.child_type(value) - - {state, var_id} = - if child_type do - ensure_var_id(state, pid, value) - else - {state, 0} - end - - json = %{ - "name" => to_string(name), - "value" => inspect(value), - "variablesReference" => var_id, - "type" => Variables.type(value) - } + {state, var_id} = get_variable_reference(child_type, state, pid, value) json = - case child_type do - :indexed -> Map.put(json, "indexedVariables", num_children) - :named -> Map.put(json, "namedVariables", num_children) - nil -> json - end + %{ + "name" => to_string(name), + "value" => inspect(value), + "variablesReference" => var_id + } + |> maybe_append_children_number(state.client_info, child_type, value) + |> maybe_append_variable_type(state.client_info, value) {state, result ++ [json]} end) end + defp get_variable_reference(nil, state, _pid, _value), do: {state, 0} + + defp get_variable_reference(_child_type, state, pid, value), + do: ensure_var_id(state, pid, value) + + defp maybe_append_children_number(map, %{"supportsVariablePaging" => true}, atom, value) + when atom in [:indexed, :named], + do: Map.put(map, Atom.to_string(atom) <> "Variables", Variables.num_children(value)) + + defp maybe_append_children_number(map, _, _, _value), do: map + + defp maybe_append_variable_type(map, %{"supportsVariableType" => true}, value) do + Map.put(map, "type", Variables.type(value)) + end + + defp maybe_append_variable_type(map, _, _value), do: map + defp evaluate_code_expression(expr, bindings, timeout) do task = Task.async(fn -> @@ -678,10 +699,15 @@ defmodule ElixirLS.Debugger.Server do end end - defp all_variables(paused_processes) do + defp all_variables(paused_processes, nil) do paused_processes - |> Enum.flat_map(fn {_pid, %PausedProcess{} = paused_process} -> - paused_process.frames |> Map.values() + |> Enum.flat_map(fn + {:evaluator, _} -> + # TODO setVariable? + [] + + {_pid, %PausedProcess{} = paused_process} -> + paused_process.frames |> Map.values() end) |> Enum.filter(&match?(%Frame{bindings: bindings} when is_map(bindings), &1)) |> Enum.flat_map(fn %Frame{bindings: bindings} -> @@ -689,23 +715,37 @@ defmodule ElixirLS.Debugger.Server do end) end + defp all_variables(paused_processes, frame_id) do + case find_frame(paused_processes, frame_id) do + {_pid, %Frame{bindings: bindings}} when is_map(bindings) -> + Binding.to_elixir_variable_names(bindings) + + _ -> + [] + end + end + defp find_var(paused_processes, var_id) do - Enum.find_value(paused_processes, fn {pid, %PausedProcess{} = paused_process} -> - if Map.has_key?(paused_process.vars, var_id) do - {pid, paused_process.vars[var_id]} + Enum.find_value(paused_processes, fn {pid, %{vars: vars}} -> + if Map.has_key?(vars, var_id) do + {pid, vars[var_id]} end end) end defp find_frame(paused_processes, frame_id) do - Enum.find_value(paused_processes, fn {pid, %PausedProcess{} = paused_process} -> - if Map.has_key?(paused_process.frames, frame_id) do - {pid, paused_process.frames[frame_id]} - end + Enum.find_value(paused_processes, fn + {pid, %{frames: frames}} -> + if Map.has_key?(frames, frame_id) do + {pid, frames[frame_id]} + end + + {:evaluator, _} -> + nil end) end - defp ensure_thread_id(state = %__MODULE__{}, pid) do + defp ensure_thread_id(state = %__MODULE__{}, pid) when is_pid(pid) do if Map.has_key?(state.threads_inverse, pid) do {state, state.threads_inverse[pid]} else @@ -724,7 +764,7 @@ defmodule ElixirLS.Debugger.Server do end) end - defp ensure_var_id(state = %__MODULE__{}, pid, var) do + defp ensure_var_id(state = %__MODULE__{}, pid, var) when is_pid(pid) or pid == :evaluator do unless Map.has_key?(state.paused_processes, pid) do raise ArgumentError, message: "paused process #{inspect(pid)} not found" end @@ -747,7 +787,7 @@ defmodule ElixirLS.Debugger.Server do end) end - defp ensure_frame_id(state = %__MODULE__{}, pid, %Frame{} = frame) do + defp ensure_frame_id(state = %__MODULE__{}, pid, %Frame{} = frame) when is_pid(pid) do unless Map.has_key?(state.paused_processes, pid) do raise ArgumentError, message: "paused process #{inspect(pid)} not found" end diff --git a/apps/elixir_ls_debugger/test/debugger_test.exs b/apps/elixir_ls_debugger/test/debugger_test.exs index fd1a87619..b190afca4 100644 --- a/apps/elixir_ls_debugger/test/debugger_test.exs +++ b/apps/elixir_ls_debugger/test/debugger_test.exs @@ -93,7 +93,14 @@ defmodule ElixirLS.Debugger.ServerTest do @tag :fixture test "basic debugging", %{server: server} do in_fixture(__DIR__, "mix_project", fn -> - Server.receive_packet(server, initialize_req(1, %{})) + Server.receive_packet( + server, + initialize_req(1, %{ + "supportsVariablePaging" => true, + "supportsVariableType" => true + }) + ) + assert_receive(response(_, 1, "initialize", %{"supportsConfigurationDoneRequest" => true})) Server.receive_packet( From 58f07d87341d5dba2f77eae0f0a68af3fce57c44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Samson?= Date: Sun, 13 Feb 2022 11:27:24 +0100 Subject: [PATCH 003/125] Add support for pause and terminateThread requests in debugger (#675) * add support for terminateThreads request * add support for pause * format --- .../lib/debugger/protocol.ex | 12 ++ .../elixir_ls_debugger/lib/debugger/server.ex | 77 ++++++++-- .../lib/debugger/stacktrace.ex | 2 +- .../elixir_ls_debugger/test/debugger_test.exs | 143 ++++++++++++++++++ .../fixtures/mix_project/lib/mix_project.ex | 5 + 5 files changed, 224 insertions(+), 15 deletions(-) diff --git a/apps/elixir_ls_debugger/lib/debugger/protocol.ex b/apps/elixir_ls_debugger/lib/debugger/protocol.ex index b5f1a9e38..b7b231915 100644 --- a/apps/elixir_ls_debugger/lib/debugger/protocol.ex +++ b/apps/elixir_ls_debugger/lib/debugger/protocol.ex @@ -55,6 +55,18 @@ defmodule ElixirLS.Debugger.Protocol do end end + defmacro terminate_threads_req(seq, thread_ids) do + quote do + request(unquote(seq), "terminateThreads", %{"threadIds" => unquote(thread_ids)}) + end + end + + defmacro pause_req(seq, thread_id) do + quote do + request(unquote(seq), "pause", %{"threadId" => unquote(thread_id)}) + end + end + defmacro stacktrace_req(seq, thread_id) do quote do request(unquote(seq), "stackTrace", %{"threadId" => unquote(thread_id)}) diff --git a/apps/elixir_ls_debugger/lib/debugger/server.ex b/apps/elixir_ls_debugger/lib/debugger/server.ex index 125fd78e8..b5e9d99b7 100644 --- a/apps/elixir_ls_debugger/lib/debugger/server.ex +++ b/apps/elixir_ls_debugger/lib/debugger/server.ex @@ -71,6 +71,10 @@ defmodule ElixirLS.Debugger.Server do GenServer.cast(server, {:breakpoint_reached, pid}) end + def paused(pid, server) do + GenServer.cast(server, {:paused, pid}) + end + ## Server Callbacks @impl GenServer @@ -116,7 +120,8 @@ defmodule ElixirLS.Debugger.Server do end @impl GenServer - def handle_cast({:breakpoint_reached, pid}, state = %__MODULE__{}) do + def handle_cast({event, pid}, state = %__MODULE__{}) + when event in [:breakpoint_reached, :paused] do # when debugged pid exits we get another breakpoint reached message (at least on OTP 23) # check if process is alive to not debug dead ones state = @@ -128,12 +133,23 @@ defmodule ElixirLS.Debugger.Server do paused_process = %PausedProcess{stack: Stacktrace.get(pid), ref: ref} state = put_in(state.paused_processes[pid], paused_process) - # Debugger Adapter Protocol requires us to return 'function breakpoint' reason - # but we can't tell what kind of a breakpoint was hit - body = %{"reason" => "breakpoint", "threadId" => thread_id, "allThreadsStopped" => false} + reason = + case event do + :breakpoint_reached -> + # Debugger Adapter Protocol requires us to return 'step' | 'breakpoint' | 'exception' | 'pause' | 'entry' | 'goto' + # | 'function breakpoint' | 'data breakpoint' | 'instruction breakpoint' + # but we can't tell what kind of a breakpoint was hit + "breakpoint" + + :paused -> + "pause" + end + + body = %{"reason" => reason, "threadId" => thread_id, "allThreadsStopped" => false} Output.send_event("stopped", body) state else + Process.monitor(pid) state end @@ -169,19 +185,21 @@ defmodule ElixirLS.Debugger.Server do "debugged process #{inspect(pid)} exited with reason #{Exception.format_exit(reason)}" ) - thread_id = state.threads_inverse[pid] + {thread_id, threads_inverse} = state.threads_inverse |> Map.pop(pid) state = remove_paused_process(state, pid) state = %{ state | threads: state.threads |> Map.delete(thread_id), - threads_inverse: state.threads_inverse |> Map.delete(pid) + threads_inverse: threads_inverse } - Output.send_event("thread", %{ - "reason" => "exited", - "threadId" => thread_id - }) + if thread_id do + Output.send_event("thread", %{ + "reason" => "exited", + "threadId" => thread_id + }) + end {:noreply, state} end @@ -361,8 +379,7 @@ defmodule ElixirLS.Debugger.Server do end defp handle_request(configuration_done_req(_), state = %__MODULE__{}) do - server = :erlang.process_info(self())[:registered_name] || self() - :int.auto_attach([:break], {__MODULE__, :breakpoint_reached, [server]}) + :int.auto_attach([:break], build_attach_mfa(:breakpoint_reached)) task = state.config["task"] || Mix.Project.config()[:default_task] args = state.config["taskArgs"] || [] @@ -396,6 +413,28 @@ defmodule ElixirLS.Debugger.Server do {%{"threads" => threads}, state} end + defp handle_request(terminate_threads_req(_, thread_ids), state = %__MODULE__{}) do + for {id, pid} <- state.threads, + id in thread_ids do + # :kill is untrappable + # do not need to cleanup here, :DOWN message handler will do it + Process.monitor(pid) + Process.exit(pid, :kill) + end + + {%{}, state} + end + + defp handle_request(pause_req(_, thread_id), state = %__MODULE__{}) do + pid = state.threads[thread_id] + + if pid do + :int.attach(pid, build_attach_mfa(:paused)) + end + + {%{}, state} + end + defp handle_request( request(_, "stackTrace", %{"threadId" => thread_id} = args), state = %__MODULE__{} @@ -623,8 +662,12 @@ defmodule ElixirLS.Debugger.Server do end defp remove_paused_process(state = %__MODULE__{}, pid) do - {process = %PausedProcess{}, paused_processes} = Map.pop(state.paused_processes, pid) - true = Process.demonitor(process.ref, [:flush]) + {process, paused_processes} = Map.pop(state.paused_processes, pid) + + if process do + true = Process.demonitor(process.ref, [:flush]) + end + %__MODULE__{state | paused_processes: paused_processes} end @@ -904,6 +947,7 @@ defmodule ElixirLS.Debugger.Server do "supportsExceptionOptions" => false, "supportsValueFormattingOptions" => false, "supportsExceptionInfoRequest" => false, + "supportsTerminateThreadsRequest" => true, "supportTerminateDebuggee" => false } end @@ -1144,4 +1188,9 @@ defmodule ElixirLS.Debugger.Server do 0 end end + + defp build_attach_mfa(reason) do + server = Process.info(self())[:registered_name] || self() + {__MODULE__, reason, [server]} + end end diff --git a/apps/elixir_ls_debugger/lib/debugger/stacktrace.ex b/apps/elixir_ls_debugger/lib/debugger/stacktrace.ex index a5a887364..677301614 100644 --- a/apps/elixir_ls_debugger/lib/debugger/stacktrace.ex +++ b/apps/elixir_ls_debugger/lib/debugger/stacktrace.ex @@ -52,7 +52,7 @@ defmodule ElixirLS.Debugger.Stacktrace do [first_frame | other_frames] error -> - IO.warn("Failed to obtain meta pid for #{inspect(pid)}: #{inspect(error)}") + IO.warn("Failed to obtain meta for pid #{inspect(pid)}: #{inspect(error)}") [] end end diff --git a/apps/elixir_ls_debugger/test/debugger_test.exs b/apps/elixir_ls_debugger/test/debugger_test.exs index b190afca4..da6601479 100644 --- a/apps/elixir_ls_debugger/test/debugger_test.exs +++ b/apps/elixir_ls_debugger/test/debugger_test.exs @@ -476,6 +476,149 @@ defmodule ElixirLS.Debugger.ServerTest do end) end + @tag :fixture + test "terminate threads", %{server: server} do + in_fixture(__DIR__, "mix_project", fn -> + Server.receive_packet(server, initialize_req(1, %{})) + assert_receive(response(_, 1, "initialize", %{"supportsConfigurationDoneRequest" => true})) + + Server.receive_packet( + server, + launch_req(2, %{ + "request" => "launch", + "type" => "mix_task", + "task" => "run", + "taskArgs" => ["-e", "MixProject.Some.sleep()"], + "projectDir" => File.cwd!() + }) + ) + + assert_receive(response(_, 2, "launch", %{}), 5000) + assert_receive(event(_, "initialized", %{})) + + Server.receive_packet(server, request(5, "configurationDone", %{})) + assert_receive(response(_, 5, "configurationDone", %{})) + Process.sleep(1000) + Server.receive_packet(server, request(6, "threads", %{})) + assert_receive(response(_, 6, "threads", %{"threads" => threads}), 1_000) + + assert [thread_id] = + threads + |> Enum.filter(&(&1["name"] |> String.starts_with?("MixProject.Some"))) + |> Enum.map(& &1["id"]) + + Server.receive_packet(server, request(7, "terminateThreads", %{"threadIds" => [thread_id]})) + assert_receive(response(_, 7, "terminateThreads", %{}), 500) + + assert_receive event(_, "thread", %{ + "reason" => "exited", + "threadId" => ^thread_id + }), + 5000 + end) + end + + describe "pause" do + @tag :fixture + test "alive", %{server: server} do + in_fixture(__DIR__, "mix_project", fn -> + Server.receive_packet(server, initialize_req(1, %{})) + + assert_receive( + response(_, 1, "initialize", %{"supportsConfigurationDoneRequest" => true}) + ) + + Server.receive_packet( + server, + launch_req(2, %{ + "request" => "launch", + "type" => "mix_task", + "task" => "run", + "taskArgs" => ["-e", "MixProject.Some.sleep()"], + "projectDir" => File.cwd!() + }) + ) + + assert_receive(response(_, 2, "launch", %{}), 5000) + assert_receive(event(_, "initialized", %{})) + + Server.receive_packet(server, request(5, "configurationDone", %{})) + assert_receive(response(_, 5, "configurationDone", %{})) + Process.sleep(1000) + Server.receive_packet(server, request(6, "threads", %{})) + assert_receive(response(_, 6, "threads", %{"threads" => threads}), 1_000) + + assert [thread_id] = + threads + |> Enum.filter(&(&1["name"] |> String.starts_with?("MixProject.Some"))) + |> Enum.map(& &1["id"]) + + {_, stderr} = + capture_log_and_io(:standard_error, fn -> + Server.receive_packet(server, request(7, "pause", %{"threadId" => thread_id})) + assert_receive(response(_, 7, "pause", %{}), 500) + + assert_receive event(_, "stopped", %{ + "allThreadsStopped" => false, + "reason" => "pause", + "threadId" => ^thread_id + }), + 500 + end) + + assert stderr =~ "Failed to obtain meta for pid" + end) + end + + @tag :fixture + test "dead", %{server: server} do + in_fixture(__DIR__, "mix_project", fn -> + Server.receive_packet(server, initialize_req(1, %{})) + + assert_receive( + response(_, 1, "initialize", %{"supportsConfigurationDoneRequest" => true}) + ) + + Server.receive_packet( + server, + launch_req(2, %{ + "request" => "launch", + "type" => "mix_task", + "task" => "run", + "taskArgs" => ["-e", "MixProject.Some.sleep()"], + "projectDir" => File.cwd!() + }) + ) + + assert_receive(response(_, 2, "launch", %{}), 5000) + assert_receive(event(_, "initialized", %{})) + + Server.receive_packet(server, request(5, "configurationDone", %{})) + assert_receive(response(_, 5, "configurationDone", %{})) + Process.sleep(1000) + Server.receive_packet(server, request(6, "threads", %{})) + assert_receive(response(_, 6, "threads", %{"threads" => threads}), 1_000) + + assert [thread_id] = + threads + |> Enum.filter(&(&1["name"] |> String.starts_with?("MixProject.Some"))) + |> Enum.map(& &1["id"]) + + Process.whereis(MixProject.Some) |> Process.exit(:kill) + Process.sleep(1000) + + Server.receive_packet(server, request(7, "pause", %{"threadId" => thread_id})) + assert_receive(response(_, 7, "pause", %{}), 500) + + assert_receive event(_, "thread", %{ + "reason" => "exited", + "threadId" => ^thread_id + }), + 5000 + end) + end + end + describe "breakpoints" do @tag :fixture test "sets and unsets breakpoints in erlang modules", %{server: server} do diff --git a/apps/elixir_ls_debugger/test/fixtures/mix_project/lib/mix_project.ex b/apps/elixir_ls_debugger/test/fixtures/mix_project/lib/mix_project.ex index 5bb02e70b..9e3423b35 100644 --- a/apps/elixir_ls_debugger/test/fixtures/mix_project/lib/mix_project.ex +++ b/apps/elixir_ls_debugger/test/fixtures/mix_project/lib/mix_project.ex @@ -38,4 +38,9 @@ defmodule MixProject.Some do def quadruple(x) do double(double(x)) end + + def sleep do + Supervisor.start_link([], strategy: :one_for_one, name: __MODULE__) + Process.sleep(:infinity) + end end From 6057d360934b3b7ddbb45c529f9c332b529e90de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Samson?= Date: Sat, 19 Feb 2022 12:03:09 +0100 Subject: [PATCH 004/125] Fix numerous cases of incorrect utf16 positions returned and passed into elixir_sense (#677) * add utility functions for utf8-utf16 conversions * fix numerous cases of incorrect utf16 positions --- .../lib/language_server/build.ex | 22 +- .../lib/language_server/protocol/location.ex | 26 +- .../language_server/providers/code_lens.ex | 1 + .../language_server/providers/completion.ex | 14 +- .../language_server/providers/definition.ex | 8 +- .../providers/document_symbols.ex | 53 +- .../providers/execute_command/apply_spec.ex | 1 + .../lib/language_server/providers/hover.ex | 19 +- .../providers/implementation.ex | 7 +- .../providers/on_type_formatting.ex | 4 +- .../language_server/providers/references.ex | 50 +- .../providers/signature_help.ex | 4 +- .../providers/workspace_symbols.ex | 3 + .../lib/language_server/source_file.ex | 65 +- .../test/providers/document_symbols_test.exs | 1338 ++++++++++++----- .../language_server/test/source_file_test.exs | 53 + 16 files changed, 1181 insertions(+), 487 deletions(-) diff --git a/apps/language_server/lib/language_server/build.ex b/apps/language_server/lib/language_server/build.ex index 31c0edc6c..e8d050cc2 100644 --- a/apps/language_server/lib/language_server/build.ex +++ b/apps/language_server/lib/language_server/build.ex @@ -281,6 +281,7 @@ defmodule ElixirLS.LanguageServer.Build do defp range(position, nil) when is_integer(position) do line = position - 1 + # we don't care about utf16 positions here as we send 0 %{ "start" => %{"line" => line, "character" => 0}, "end" => %{"line" => line, "character" => 0} @@ -290,23 +291,24 @@ defmodule ElixirLS.LanguageServer.Build do defp range(position, source_file) when is_integer(position) do line = position - 1 text = Enum.at(SourceFile.lines(source_file), line) || "" - start_idx = String.length(text) - String.length(String.trim_leading(text)) - length = Enum.max([String.length(String.trim(text)), 1]) - %{ - "start" => %{"line" => line, "character" => start_idx}, - "end" => %{"line" => line, "character" => start_idx + length} - } - end + start_idx = String.length(text) - String.length(String.trim_leading(text)) + 1 + length = max(String.length(String.trim(text)), 1) - defp range({start_line, start_col, end_line, end_col}, _) do %{ - "start" => %{"line" => start_line - 1, "character" => start_col}, - "end" => %{"line" => end_line - 1, "character" => end_col} + "start" => %{ + "line" => line, + "character" => SourceFile.elixir_character_to_lsp(text, start_idx) + }, + "end" => %{ + "line" => line, + "character" => SourceFile.elixir_character_to_lsp(text, start_idx + length) + } } end defp range(_, nil) do + # we don't care about utf16 positions here as we send 0 %{"start" => %{"line" => 0, "character" => 0}, "end" => %{"line" => 0, "character" => 0}} end diff --git a/apps/language_server/lib/language_server/protocol/location.ex b/apps/language_server/lib/language_server/protocol/location.ex index 8f3bf9baa..6e76f4a09 100644 --- a/apps/language_server/lib/language_server/protocol/location.ex +++ b/apps/language_server/lib/language_server/protocol/location.ex @@ -8,26 +8,30 @@ defmodule ElixirLS.LanguageServer.Protocol.Location do defstruct [:uri, :range] alias ElixirLS.LanguageServer.SourceFile - alias ElixirLS.LanguageServer.Protocol + require ElixirLS.LanguageServer.Protocol, as: Protocol - def new(%ElixirSense.Location{file: file, line: line, column: column}, uri) do + def new( + %ElixirSense.Location{file: file, line: line, column: column}, + current_file_uri, + current_file_text + ) do uri = case file do - nil -> uri + nil -> current_file_uri _ -> SourceFile.path_to_uri(file) end - # LSP messages are 0 indexed whilst elixir/erlang is 1 indexed. - # Guard against malformed line or column values. - line = max(line - 1, 0) - column = max(column - 1, 0) + text = + case file do + nil -> current_file_text + file -> File.read!(file) + end + + {line, column} = SourceFile.elixir_position_to_lsp(text, {line, column}) %Protocol.Location{ uri: uri, - range: %{ - "start" => %{"line" => line, "character" => column}, - "end" => %{"line" => line, "character" => column} - } + range: Protocol.range(line, column, line, column) } end end diff --git a/apps/language_server/lib/language_server/providers/code_lens.ex b/apps/language_server/lib/language_server/providers/code_lens.ex index 0d035d772..6367677c7 100644 --- a/apps/language_server/lib/language_server/providers/code_lens.ex +++ b/apps/language_server/lib/language_server/providers/code_lens.ex @@ -17,6 +17,7 @@ defmodule ElixirLS.LanguageServer.Providers.CodeLens do def build_code_lens(line, title, command, argument) do %{ + # we don't care about utf16 positions here as we send 0 "range" => range(line - 1, 0, line - 1, 0), "command" => %{ "title" => title, diff --git a/apps/language_server/lib/language_server/providers/completion.ex b/apps/language_server/lib/language_server/providers/completion.ex index 7ea6f767a..b50f3c1d6 100644 --- a/apps/language_server/lib/language_server/providers/completion.ex +++ b/apps/language_server/lib/language_server/providers/completion.ex @@ -90,16 +90,20 @@ defmodule ElixirLS.LanguageServer.Providers.Completion do |> SourceFile.lines() |> Enum.at(line) - text_before_cursor = String.slice(line_text, 0, character) - text_after_cursor = String.slice(line_text, character..-1) + # convert to 1 based utf8 position + line = line + 1 + character = SourceFile.lsp_character_to_elixir(line_text, character) + + text_before_cursor = String.slice(line_text, 0, character - 1) + text_after_cursor = String.slice(line_text, (character - 1)..-1) prefix = get_prefix(text_before_cursor) # TODO: Don't call into here directly # Can we use ElixirSense.Providers.Suggestion? ElixirSense.suggestions/3 env = - ElixirSense.Core.Parser.parse_string(text, true, true, line + 1) - |> ElixirSense.Core.Metadata.get_env(line + 1) + ElixirSense.Core.Parser.parse_string(text, true, true, line) + |> ElixirSense.Core.Metadata.get_env(line) scope = case env.scope do @@ -135,7 +139,7 @@ defmodule ElixirLS.LanguageServer.Providers.Completion do } items = - ElixirSense.suggestions(text, line + 1, character + 1) + ElixirSense.suggestions(text, line, character) |> maybe_reject_derived_functions(context, options) |> Enum.map(&from_completion_item(&1, context, options)) |> maybe_add_do(context) diff --git a/apps/language_server/lib/language_server/providers/definition.ex b/apps/language_server/lib/language_server/providers/definition.ex index 3b64c0f92..65dd62128 100644 --- a/apps/language_server/lib/language_server/providers/definition.ex +++ b/apps/language_server/lib/language_server/providers/definition.ex @@ -3,16 +3,18 @@ defmodule ElixirLS.LanguageServer.Providers.Definition do Go-to-definition provider utilizing Elixir Sense """ - alias ElixirLS.LanguageServer.Protocol + alias ElixirLS.LanguageServer.{Protocol, SourceFile} def definition(uri, text, line, character) do + {line, character} = SourceFile.lsp_position_to_elixr(text, {line, character}) + result = - case ElixirSense.definition(text, line + 1, character + 1) do + case ElixirSense.definition(text, line, character) do nil -> nil %ElixirSense.Location{} = location -> - Protocol.Location.new(location, uri) + Protocol.Location.new(location, uri, text) end {:ok, result} diff --git a/apps/language_server/lib/language_server/providers/document_symbols.ex b/apps/language_server/lib/language_server/providers/document_symbols.ex index 0df6357ac..9bb4b3ae1 100644 --- a/apps/language_server/lib/language_server/providers/document_symbols.ex +++ b/apps/language_server/lib/language_server/providers/document_symbols.ex @@ -6,7 +6,8 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbols do """ alias ElixirLS.LanguageServer.Providers.SymbolUtils - alias ElixirLS.LanguageServer.Protocol + alias ElixirLS.LanguageServer.SourceFile + require ElixirLS.LanguageServer.Protocol, as: Protocol defmodule Info do defstruct [:type, :name, :location, :children] @@ -25,22 +26,22 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbols do def symbols(uri, text, hierarchical) do case list_symbols(text) do {:ok, symbols} -> - {:ok, build_symbols(symbols, uri, hierarchical)} + {:ok, build_symbols(symbols, uri, text, hierarchical)} {:error, :compilation_error} -> {:error, :server_error, "[DocumentSymbols] Compilation error while parsing source file"} end end - defp build_symbols(symbols, uri, hierarchical) + defp build_symbols(symbols, uri, text, hierarchical) - defp build_symbols(symbols, uri, true) do - Enum.map(symbols, &build_symbol_information_hierarchical(uri, &1)) + defp build_symbols(symbols, uri, text, true) do + Enum.map(symbols, &build_symbol_information_hierarchical(uri, text, &1)) end - defp build_symbols(symbols, uri, false) do + defp build_symbols(symbols, uri, text, false) do symbols - |> Enum.map(&build_symbol_information_flat(uri, &1)) + |> Enum.map(&build_symbol_information_flat(uri, text, &1)) |> List.flatten() end @@ -283,25 +284,27 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbols do defp extract_symbol(_, _), do: nil - defp build_symbol_information_hierarchical(uri, info) when is_list(info), - do: Enum.map(info, &build_symbol_information_hierarchical(uri, &1)) + defp build_symbol_information_hierarchical(uri, text, info) when is_list(info), + do: Enum.map(info, &build_symbol_information_hierarchical(uri, text, &1)) + + defp build_symbol_information_hierarchical(uri, text, %Info{} = info) do + range = location_to_range(info.location, text) - defp build_symbol_information_hierarchical(uri, %Info{} = info) do %Protocol.DocumentSymbol{ name: info.name, kind: SymbolUtils.symbol_kind_to_code(info.type), - range: location_to_range(info.location), - selectionRange: location_to_range(info.location), - children: build_symbol_information_hierarchical(uri, info.children) + range: range, + selectionRange: range, + children: build_symbol_information_hierarchical(uri, text, info.children) } end - defp build_symbol_information_flat(uri, info, parent_name \\ nil) + defp build_symbol_information_flat(uri, text, info, parent_name \\ nil) - defp build_symbol_information_flat(uri, info, parent_name) when is_list(info), - do: Enum.map(info, &build_symbol_information_flat(uri, &1, parent_name)) + defp build_symbol_information_flat(uri, text, info, parent_name) when is_list(info), + do: Enum.map(info, &build_symbol_information_flat(uri, text, &1, parent_name)) - defp build_symbol_information_flat(uri, %Info{} = info, parent_name) do + defp build_symbol_information_flat(uri, text, %Info{} = info, parent_name) do case info.children do [_ | _] -> [ @@ -310,11 +313,11 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbols do kind: SymbolUtils.symbol_kind_to_code(info.type), location: %{ uri: uri, - range: location_to_range(info.location) + range: location_to_range(info.location, text) }, containerName: parent_name } - | Enum.map(info.children, &build_symbol_information_flat(uri, &1, info.name)) + | Enum.map(info.children, &build_symbol_information_flat(uri, text, &1, info.name)) ] _ -> @@ -323,18 +326,18 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbols do kind: SymbolUtils.symbol_kind_to_code(info.type), location: %{ uri: uri, - range: location_to_range(info.location) + range: location_to_range(info.location, text) }, containerName: parent_name } end end - defp location_to_range(location) do - %{ - start: %{line: location[:line] - 1, character: location[:column] - 1}, - end: %{line: location[:line] - 1, character: location[:column] - 1} - } + defp location_to_range(location, text) do + {line, character} = + SourceFile.elixir_position_to_lsp(text, {location[:line], location[:column]}) + + Protocol.range(line, character, line, character) end defp extract_module_name(protocol: protocol, implementations: implementations) do diff --git a/apps/language_server/lib/language_server/providers/execute_command/apply_spec.ex b/apps/language_server/lib/language_server/providers/execute_command/apply_spec.ex index 96fbdae2b..12b8d34fc 100644 --- a/apps/language_server/lib/language_server/providers/execute_command/apply_spec.ex +++ b/apps/language_server/lib/language_server/providers/execute_command/apply_spec.ex @@ -78,6 +78,7 @@ defmodule ElixirLS.LanguageServer.Providers.ExecuteCommand.ApplySpec do "label" => "Add @spec to #{mod}.#{fun}/#{arity}", "edit" => %{ "changes" => %{ + # we don't care about utf16 positions here as we send 0 uri => [%{"range" => range(line - 1, 0, line - 1, 0), "newText" => formatted}] } } diff --git a/apps/language_server/lib/language_server/providers/hover.ex b/apps/language_server/lib/language_server/providers/hover.ex index 1c61e8ab1..c2e470762 100644 --- a/apps/language_server/lib/language_server/providers/hover.ex +++ b/apps/language_server/lib/language_server/providers/hover.ex @@ -1,5 +1,6 @@ defmodule ElixirLS.LanguageServer.Providers.Hover do alias ElixirLS.LanguageServer.SourceFile + import ElixirLS.LanguageServer.Protocol @moduledoc """ Hover provider utilizing Elixir Sense @@ -17,14 +18,16 @@ defmodule ElixirLS.LanguageServer.Providers.Hover do |> Enum.map(fn x -> "lib/#{x}/lib" end) def hover(text, line, character, project_dir) do + {line, character} = SourceFile.lsp_position_to_elixr(text, {line, character}) + response = - case ElixirSense.docs(text, line + 1, character + 1) do + case ElixirSense.docs(text, line, character) do %{subject: ""} -> nil %{subject: subject, docs: docs} -> - line_text = Enum.at(SourceFile.lines(text), line) - range = highlight_range(line_text, line, character, subject) + line_text = Enum.at(SourceFile.lines(text), line - 1) + range = highlight_range(line_text, line - 1, character - 1, subject) %{"contents" => contents(docs, subject, project_dir), "range" => range} end @@ -45,10 +48,12 @@ defmodule ElixirLS.LanguageServer.Providers.Hover do Enum.find_value(regex_ranges, fn [{start, length}] when start <= character and character <= start + length -> - %{ - "start" => %{"line" => line, "character" => start}, - "end" => %{"line" => line, "character" => start + length} - } + range( + line, + SourceFile.elixir_character_to_lsp(line_text, start + 1), + line, + SourceFile.elixir_character_to_lsp(line_text, start + 1 + length) + ) _ -> nil diff --git a/apps/language_server/lib/language_server/providers/implementation.ex b/apps/language_server/lib/language_server/providers/implementation.ex index 746235a54..920139901 100644 --- a/apps/language_server/lib/language_server/providers/implementation.ex +++ b/apps/language_server/lib/language_server/providers/implementation.ex @@ -3,11 +3,12 @@ defmodule ElixirLS.LanguageServer.Providers.Implementation do Go-to-implementation provider utilizing Elixir Sense """ - alias ElixirLS.LanguageServer.Protocol + alias ElixirLS.LanguageServer.{Protocol, SourceFile} def implementation(uri, text, line, character) do - locations = ElixirSense.implementations(text, line + 1, character + 1) - results = for location <- locations, do: Protocol.Location.new(location, uri) + {line, character} = SourceFile.lsp_position_to_elixr(text, {line, character}) + locations = ElixirSense.implementations(text, line, character) + results = for location <- locations, do: Protocol.Location.new(location, uri, text) {:ok, results} end diff --git a/apps/language_server/lib/language_server/providers/on_type_formatting.ex b/apps/language_server/lib/language_server/providers/on_type_formatting.ex index 3b374ca8f..6665b1376 100644 --- a/apps/language_server/lib/language_server/providers/on_type_formatting.ex +++ b/apps/language_server/lib/language_server/providers/on_type_formatting.ex @@ -10,7 +10,8 @@ defmodule ElixirLS.LanguageServer.Providers.OnTypeFormatting do alias ElixirLS.LanguageServer.SourceFile import ElixirLS.LanguageServer.Protocol - def format(%SourceFile{} = source_file, line, character, "\n", _options) do + def format(%SourceFile{} = source_file, line, character, "\n", _options) when line >= 1 do + # we don't care about utf16 positions here as we only pass character back to client lines = SourceFile.lines(source_file) prev_line = Enum.at(lines, line - 1) @@ -69,6 +70,7 @@ defmodule ElixirLS.LanguageServer.Providers.OnTypeFormatting do # In VS Code, currently, the cursor jumps strangely if the current line is blank and we try to # insert a newline at the current position, so unfortunately, we have to check for that. defp insert_end_edit(indentation, line, character, insert_on_next_line?) do + # we don't care about utf16 positions here as we either use 0 or what the client sent if insert_on_next_line? do {range(line + 1, 0, line + 1, 0), "#{indentation}end\n"} else diff --git a/apps/language_server/lib/language_server/providers/references.ex b/apps/language_server/lib/language_server/providers/references.ex index 293982e02..b2612ea9e 100644 --- a/apps/language_server/lib/language_server/providers/references.ex +++ b/apps/language_server/lib/language_server/providers/references.ex @@ -11,26 +11,34 @@ defmodule ElixirLS.LanguageServer.Providers.References do """ alias ElixirLS.LanguageServer.{SourceFile, Build} + import ElixirLS.LanguageServer.Protocol def references(text, uri, line, character, _include_declaration) do + {line, character} = SourceFile.lsp_position_to_elixr(text, {line, character}) + Build.with_build_lock(fn -> - ElixirSense.references(text, line + 1, character + 1) + ElixirSense.references(text, line, character) |> Enum.map(fn elixir_sense_reference -> elixir_sense_reference - |> build_reference(uri) - |> build_loc() + |> build_reference(uri, text) end) - |> Enum.filter(&has_uri?/1) end) end - defp build_reference(ref, current_file_uri) do + defp build_reference(ref, current_file_uri, current_file_text) do + text = get_text(ref, current_file_text) + + {start_line, start_column} = + SourceFile.elixir_position_to_lsp(text, {ref.range.start.line, ref.range.start.column}) + + {end_line, end_column} = + SourceFile.elixir_position_to_lsp(text, {ref.range.end.line, ref.range.end.column}) + + range = range(start_line, start_column, end_line, end_column) + %{ - range: %{ - start: %{line: ref.range.start.line, column: ref.range.start.column}, - end: %{line: ref.range.end.line, column: ref.range.end.column} - }, - uri: build_uri(ref, current_file_uri) + "range" => range, + "uri" => build_uri(ref, current_file_uri) } end @@ -42,25 +50,13 @@ defmodule ElixirLS.LanguageServer.Providers.References do # ElixirSense returns a plain path (e.g. "/home/bob/my_app/lib/a.ex") as # the "uri" so we convert it to an actual uri path when is_binary(path) -> SourceFile.path_to_uri(path) - _ -> nil end end - defp has_uri?(reference), do: !is_nil(reference["uri"]) - - defp build_loc(reference) do - # Adjust for ElixirSense 1-based indexing - line_start = reference.range.start.line - 1 - line_end = reference.range.end.line - 1 - column_start = reference.range.start.column - 1 - column_end = reference.range.end.column - 1 - - %{ - "uri" => reference.uri, - "range" => %{ - "start" => %{"line" => line_start, "character" => column_start}, - "end" => %{"line" => line_end, "character" => column_end} - } - } + def get_text(elixir_sense_ref, current_file_text) do + case elixir_sense_ref.uri do + nil -> current_file_text + path when is_binary(path) -> File.read!(path) + end end end diff --git a/apps/language_server/lib/language_server/providers/signature_help.ex b/apps/language_server/lib/language_server/providers/signature_help.ex index 296312d49..9b8ae94e9 100644 --- a/apps/language_server/lib/language_server/providers/signature_help.ex +++ b/apps/language_server/lib/language_server/providers/signature_help.ex @@ -4,8 +4,10 @@ defmodule ElixirLS.LanguageServer.Providers.SignatureHelp do def trigger_characters(), do: ["(", ","] def signature(%SourceFile{} = source_file, line, character) do + {line, character} = SourceFile.lsp_position_to_elixr(source_file.text, {line, character}) + response = - case ElixirSense.signature(source_file.text, line + 1, character + 1) do + case ElixirSense.signature(source_file.text, line, character) do %{active_param: active_param, signatures: signatures} -> %{ "activeSignature" => 0, diff --git a/apps/language_server/lib/language_server/providers/workspace_symbols.ex b/apps/language_server/lib/language_server/providers/workspace_symbols.ex index ef28ff277..91a22ccf6 100644 --- a/apps/language_server/lib/language_server/providers/workspace_symbols.ex +++ b/apps/language_server/lib/language_server/providers/workspace_symbols.ex @@ -512,15 +512,18 @@ defmodule ElixirLS.LanguageServer.Providers.WorkspaceSymbols do @spec build_range(nil | erl_location_t) :: range_t defp build_range(nil) do + # we don't care about utf16 positions here as we send 0 %{ start: %{line: 0, character: 0}, end: %{line: 1, character: 0} } end + # it's not worth to present column info here defp build_range({line, _column}), do: build_range(line) defp build_range(line) do + # we don't care about utf16 positions here as we send 0 %{ start: %{line: max(line - 1, 0), character: 0}, end: %{line: line, character: 0} diff --git a/apps/language_server/lib/language_server/source_file.ex b/apps/language_server/lib/language_server/source_file.ex index 5d3782623..5f02ea1c4 100644 --- a/apps/language_server/lib/language_server/source_file.ex +++ b/apps/language_server/lib/language_server/source_file.ex @@ -172,10 +172,7 @@ defmodule ElixirLS.LanguageServer.SourceFile do |> List.last() |> line_length_utf16() - %{ - "start" => %{"line" => 0, "character" => 0}, - "end" => %{"line" => Enum.count(lines) - 1, "character" => utf16_size} - } + range(0, 0, Enum.count(lines) - 1, utf16_size) end def line_length_utf16(line) do @@ -373,4 +370,64 @@ defmodule ElixirLS.LanguageServer.SourceFile do end defp remove_indentation(lines, _), do: lines + + def lsp_character_to_elixir(_utf8_line, lsp_character) when lsp_character <= 0, do: 1 + + def lsp_character_to_elixir(utf8_line, lsp_character) do + utf16_line = + utf8_line + |> characters_to_binary!(:utf8, :utf16) + + byte_size = byte_size(utf16_line) + + # if character index is over the length of the string assume we pad it with spaces (1 byte in utf8) + diff = div(max(lsp_character * 2 - byte_size, 0), 2) + + utf8_character = + utf16_line + |> (&binary_part( + &1, + 0, + min(lsp_character * 2, byte_size) + )).() + |> characters_to_binary!(:utf16, :utf8) + |> String.length() + + utf8_character + 1 + diff + end + + def lsp_position_to_elixr(_urf8_text, {lsp_line, lsp_character}) when lsp_character <= 0, + do: {max(lsp_line + 1, 1), 1} + + def lsp_position_to_elixr(urf8_text, {lsp_line, lsp_character}) do + utf8_character = + lines(urf8_text) + |> Enum.at(max(lsp_line, 0)) + |> lsp_character_to_elixir(lsp_character) + + {lsp_line + 1, utf8_character} + end + + def elixir_character_to_lsp(_utf8_line, elixir_character) when elixir_character <= 1, do: 0 + + def elixir_character_to_lsp(utf8_line, elixir_character) do + utf8_line + |> String.slice(0..(elixir_character - 2)) + |> characters_to_binary!(:utf8, :utf16) + |> byte_size() + |> div(2) + end + + def elixir_position_to_lsp(_urf8_text, {elixir_line, elixir_character}) + when elixir_character <= 1, + do: {max(elixir_line - 1, 0), 0} + + def elixir_position_to_lsp(urf8_text, {elixir_line, elixir_character}) do + utf16_character = + lines(urf8_text) + |> Enum.at(max(elixir_line - 1, 0)) + |> elixir_character_to_lsp(elixir_character) + + {elixir_line - 1, utf16_character} + end end diff --git a/apps/language_server/test/providers/document_symbols_test.exs b/apps/language_server/test/providers/document_symbols_test.exs index 920d9e1f7..67948439f 100644 --- a/apps/language_server/test/providers/document_symbols_test.exs +++ b/apps/language_server/test/providers/document_symbols_test.exs @@ -41,127 +41,169 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do children: [], kind: 14, name: "@my_mod_var", - range: %{end: %{character: 9, line: 2}, start: %{character: 9, line: 2}}, + range: %{ + "end" => %{"character" => 9, "line" => 2}, + "start" => %{"character" => 9, "line" => 2} + }, selectionRange: %{ - end: %{character: 9, line: 2}, - start: %{character: 9, line: 2} + "end" => %{"character" => 9, "line" => 2}, + "start" => %{"character" => 9, "line" => 2} } }, %Protocol.DocumentSymbol{ children: [], kind: 12, name: "def my_fn(arg)", - range: %{end: %{character: 12, line: 3}, start: %{character: 12, line: 3}}, + range: %{ + "end" => %{"character" => 12, "line" => 3}, + "start" => %{"character" => 12, "line" => 3} + }, selectionRange: %{ - end: %{character: 12, line: 3}, - start: %{character: 12, line: 3} + "end" => %{"character" => 12, "line" => 3}, + "start" => %{"character" => 12, "line" => 3} } }, %Protocol.DocumentSymbol{ children: [], kind: 12, name: "defp my_private_fn(arg)", - range: %{end: %{character: 13, line: 4}, start: %{character: 13, line: 4}}, + range: %{ + "end" => %{"character" => 13, "line" => 4}, + "start" => %{"character" => 13, "line" => 4} + }, selectionRange: %{ - end: %{character: 13, line: 4}, - start: %{character: 13, line: 4} + "end" => %{"character" => 13, "line" => 4}, + "start" => %{"character" => 13, "line" => 4} } }, %Protocol.DocumentSymbol{ children: [], kind: 12, name: "defmacro my_macro()", - range: %{end: %{character: 17, line: 5}, start: %{character: 17, line: 5}}, + range: %{ + "end" => %{"character" => 17, "line" => 5}, + "start" => %{"character" => 17, "line" => 5} + }, selectionRange: %{ - end: %{character: 17, line: 5}, - start: %{character: 17, line: 5} + "end" => %{"character" => 17, "line" => 5}, + "start" => %{"character" => 17, "line" => 5} } }, %Protocol.DocumentSymbol{ children: [], kind: 12, name: "defmacrop my_private_macro()", - range: %{end: %{character: 18, line: 6}, start: %{character: 18, line: 6}}, + range: %{ + "end" => %{"character" => 18, "line" => 6}, + "start" => %{"character" => 18, "line" => 6} + }, selectionRange: %{ - end: %{character: 18, line: 6}, - start: %{character: 18, line: 6} + "end" => %{"character" => 18, "line" => 6}, + "start" => %{"character" => 18, "line" => 6} } }, %Protocol.DocumentSymbol{ children: [], kind: 12, name: "defguard my_guard(a)", - range: %{end: %{character: 17, line: 7}, start: %{character: 17, line: 7}}, + range: %{ + "end" => %{"character" => 17, "line" => 7}, + "start" => %{"character" => 17, "line" => 7} + }, selectionRange: %{ - end: %{character: 17, line: 7}, - start: %{character: 17, line: 7} + "end" => %{"character" => 17, "line" => 7}, + "start" => %{"character" => 17, "line" => 7} } }, %Protocol.DocumentSymbol{ children: [], kind: 12, name: "defguardp my_private_guard(a)", - range: %{end: %{character: 18, line: 8}, start: %{character: 18, line: 8}}, + range: %{ + "end" => %{"character" => 18, "line" => 8}, + "start" => %{"character" => 18, "line" => 8} + }, selectionRange: %{ - end: %{character: 18, line: 8}, - start: %{character: 18, line: 8} + "end" => %{"character" => 18, "line" => 8}, + "start" => %{"character" => 18, "line" => 8} } }, %Protocol.DocumentSymbol{ children: [], kind: 12, name: "defdelegate my_delegate(list)", - range: %{end: %{character: 20, line: 9}, start: %{character: 20, line: 9}}, + range: %{ + "end" => %{"character" => 20, "line" => 9}, + "start" => %{"character" => 20, "line" => 9} + }, selectionRange: %{ - end: %{character: 20, line: 9}, - start: %{character: 20, line: 9} + "end" => %{"character" => 20, "line" => 9}, + "start" => %{"character" => 20, "line" => 9} } }, %Protocol.DocumentSymbol{ children: [], kind: 12, name: "defguard my_guard", - range: %{end: %{character: 17, line: 10}, start: %{character: 17, line: 10}}, + range: %{ + "end" => %{"character" => 17, "line" => 10}, + "start" => %{"character" => 17, "line" => 10} + }, selectionRange: %{ - end: %{character: 17, line: 10}, - start: %{character: 17, line: 10} + "end" => %{"character" => 17, "line" => 10}, + "start" => %{"character" => 17, "line" => 10} } }, %Protocol.DocumentSymbol{ children: [], kind: 12, name: "def my_fn_no_arg", - range: %{end: %{character: 12, line: 11}, start: %{character: 12, line: 11}}, + range: %{ + "end" => %{"character" => 12, "line" => 11}, + "start" => %{"character" => 12, "line" => 11} + }, selectionRange: %{ - end: %{character: 12, line: 11}, - start: %{character: 12, line: 11} + "end" => %{"character" => 12, "line" => 11}, + "start" => %{"character" => 12, "line" => 11} } }, %Protocol.DocumentSymbol{ children: [], kind: 12, name: "def my_fn_with_guard(arg)", - range: %{end: %{character: 12, line: 12}, start: %{character: 12, line: 12}}, + range: %{ + "end" => %{"character" => 12, "line" => 12}, + "start" => %{"character" => 12, "line" => 12} + }, selectionRange: %{ - end: %{character: 12, line: 12}, - start: %{character: 12, line: 12} + "end" => %{"character" => 12, "line" => 12}, + "start" => %{"character" => 12, "line" => 12} } }, %Protocol.DocumentSymbol{ children: [], kind: 12, name: "def my_fn_with_more_blocks(arg)", - range: %{end: %{character: 12, line: 13}, start: %{character: 12, line: 13}}, + range: %{ + "end" => %{"character" => 12, "line" => 13}, + "start" => %{"character" => 12, "line" => 13} + }, selectionRange: %{ - end: %{character: 12, line: 13}, - start: %{character: 12, line: 13} + "end" => %{"character" => 12, "line" => 13}, + "start" => %{"character" => 12, "line" => 13} } } ], kind: 2, name: "MyModule", - range: %{end: %{character: 6, line: 1}, start: %{character: 6, line: 1}}, - selectionRange: %{end: %{character: 6, line: 1}, start: %{character: 6, line: 1}} + range: %{ + "end" => %{"character" => 6, "line" => 1}, + "start" => %{"character" => 6, "line" => 1} + }, + selectionRange: %{ + "end" => %{"character" => 6, "line" => 1}, + "start" => %{"character" => 6, "line" => 1} + } } ]} = DocumentSymbols.symbols(uri, text, true) end @@ -201,14 +243,20 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do name: "MyModule", kind: 2, location: %{ - range: %{end: %{character: 6, line: 1}, start: %{character: 6, line: 1}} + range: %{ + "end" => %{"character" => 6, "line" => 1}, + "start" => %{"character" => 6, "line" => 1} + } } }, %Protocol.SymbolInformation{ name: "@my_mod_var", kind: 14, location: %{ - range: %{end: %{character: 9, line: 2}, start: %{character: 9, line: 2}} + range: %{ + "end" => %{"character" => 9, "line" => 2}, + "start" => %{"character" => 9, "line" => 2} + } }, containerName: "MyModule" }, @@ -216,7 +264,10 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do name: "def my_fn(arg)", kind: 12, location: %{ - range: %{end: %{character: 12, line: 3}, start: %{character: 12, line: 3}} + range: %{ + "end" => %{"character" => 12, "line" => 3}, + "start" => %{"character" => 12, "line" => 3} + } }, containerName: "MyModule" }, @@ -224,7 +275,10 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do name: "defp my_private_fn(arg)", kind: 12, location: %{ - range: %{end: %{character: 13, line: 4}, start: %{character: 13, line: 4}} + range: %{ + "end" => %{"character" => 13, "line" => 4}, + "start" => %{"character" => 13, "line" => 4} + } }, containerName: "MyModule" }, @@ -232,7 +286,10 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do name: "defmacro my_macro()", kind: 12, location: %{ - range: %{end: %{character: 17, line: 5}, start: %{character: 17, line: 5}} + range: %{ + "end" => %{"character" => 17, "line" => 5}, + "start" => %{"character" => 17, "line" => 5} + } }, containerName: "MyModule" }, @@ -240,7 +297,10 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do name: "defmacrop my_private_macro()", kind: 12, location: %{ - range: %{end: %{character: 18, line: 6}, start: %{character: 18, line: 6}} + range: %{ + "end" => %{"character" => 18, "line" => 6}, + "start" => %{"character" => 18, "line" => 6} + } }, containerName: "MyModule" }, @@ -248,7 +308,10 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do name: "defguard my_guard(a)", kind: 12, location: %{ - range: %{end: %{character: 17, line: 7}, start: %{character: 17, line: 7}} + range: %{ + "end" => %{"character" => 17, "line" => 7}, + "start" => %{"character" => 17, "line" => 7} + } }, containerName: "MyModule" }, @@ -256,7 +319,10 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do name: "defguardp my_private_guard(a)", kind: 12, location: %{ - range: %{end: %{character: 18, line: 8}, start: %{character: 18, line: 8}} + range: %{ + "end" => %{"character" => 18, "line" => 8}, + "start" => %{"character" => 18, "line" => 8} + } }, containerName: "MyModule" }, @@ -264,7 +330,10 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do name: "defdelegate my_delegate(list)", kind: 12, location: %{ - range: %{end: %{character: 20, line: 9}, start: %{character: 20, line: 9}} + range: %{ + "end" => %{"character" => 20, "line" => 9}, + "start" => %{"character" => 20, "line" => 9} + } }, containerName: "MyModule" }, @@ -272,7 +341,10 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do name: "defguard my_guard", kind: 12, location: %{ - range: %{end: %{character: 17, line: 10}, start: %{character: 17, line: 10}} + range: %{ + "end" => %{"character" => 17, "line" => 10}, + "start" => %{"character" => 17, "line" => 10} + } }, containerName: "MyModule" }, @@ -280,7 +352,10 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do name: "def my_fn_no_arg", kind: 12, location: %{ - range: %{end: %{character: 12, line: 11}, start: %{character: 12, line: 11}} + range: %{ + "end" => %{"character" => 12, "line" => 11}, + "start" => %{"character" => 12, "line" => 11} + } }, containerName: "MyModule" }, @@ -288,7 +363,10 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do name: "def my_fn_with_guard(arg)", kind: 12, location: %{ - range: %{end: %{character: 12, line: 12}, start: %{character: 12, line: 12}} + range: %{ + "end" => %{"character" => 12, "line" => 12}, + "start" => %{"character" => 12, "line" => 12} + } }, containerName: "MyModule" }, @@ -296,7 +374,10 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do name: "def my_fn_with_more_blocks(arg)", kind: 12, location: %{ - range: %{end: %{character: 12, line: 13}, start: %{character: 12, line: 13}} + range: %{ + "end" => %{"character" => 12, "line" => 13}, + "start" => %{"character" => 12, "line" => 13} + } }, containerName: "MyModule" } @@ -324,36 +405,36 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do kind: 12, name: "def my_fn()", range: %{ - end: %{character: 14, line: 3}, - start: %{character: 14, line: 3} + "end" => %{"character" => 14, "line" => 3}, + "start" => %{"character" => 14, "line" => 3} }, selectionRange: %{ - end: %{character: 14, line: 3}, - start: %{character: 14, line: 3} + "end" => %{"character" => 14, "line" => 3}, + "start" => %{"character" => 14, "line" => 3} } } ], kind: 2, name: "SubModule", range: %{ - end: %{character: 8, line: 2}, - start: %{character: 8, line: 2} + "end" => %{"character" => 8, "line" => 2}, + "start" => %{"character" => 8, "line" => 2} }, selectionRange: %{ - end: %{character: 8, line: 2}, - start: %{character: 8, line: 2} + "end" => %{"character" => 8, "line" => 2}, + "start" => %{"character" => 8, "line" => 2} } } ], kind: 2, name: "MyModule", range: %{ - end: %{character: 6, line: 1}, - start: %{character: 6, line: 1} + "end" => %{"character" => 6, "line" => 1}, + "start" => %{"character" => 6, "line" => 1} }, selectionRange: %{ - end: %{character: 6, line: 1}, - start: %{character: 6, line: 1} + "end" => %{"character" => 6, "line" => 1}, + "start" => %{"character" => 6, "line" => 1} } } ]} = DocumentSymbols.symbols(uri, text, true) @@ -376,8 +457,8 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do kind: 2, location: %{ range: %{ - end: %{character: 6, line: 1}, - start: %{character: 6, line: 1} + "end" => %{"character" => 6, "line" => 1}, + "start" => %{"character" => 6, "line" => 1} } } }, @@ -386,8 +467,8 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do kind: 2, location: %{ range: %{ - end: %{character: 8, line: 2}, - start: %{character: 8, line: 2} + "end" => %{"character" => 8, "line" => 2}, + "start" => %{"character" => 8, "line" => 2} } }, containerName: "MyModule" @@ -397,8 +478,8 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do name: "def my_fn()", location: %{ range: %{ - end: %{character: 14, line: 3}, - start: %{character: 14, line: 3} + "end" => %{"character" => 14, "line" => 3}, + "start" => %{"character" => 14, "line" => 3} } }, containerName: "SubModule" @@ -426,24 +507,24 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do kind: 12, name: "def some_function()", range: %{ - end: %{character: 12, line: 2}, - start: %{character: 12, line: 2} + "end" => %{"character" => 12, "line" => 2}, + "start" => %{"character" => 12, "line" => 2} }, selectionRange: %{ - end: %{character: 12, line: 2}, - start: %{character: 12, line: 2} + "end" => %{"character" => 12, "line" => 2}, + "start" => %{"character" => 12, "line" => 2} } } ], kind: 2, name: "MyModule", range: %{ - end: %{character: 6, line: 1}, - start: %{character: 6, line: 1} + "end" => %{"character" => 6, "line" => 1}, + "start" => %{"character" => 6, "line" => 1} }, selectionRange: %{ - end: %{character: 6, line: 1}, - start: %{character: 6, line: 1} + "end" => %{"character" => 6, "line" => 1}, + "start" => %{"character" => 6, "line" => 1} } }, %Protocol.DocumentSymbol{ @@ -453,24 +534,24 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do kind: 12, name: "def some_other_function()", range: %{ - end: %{character: 12, line: 5}, - start: %{character: 12, line: 5} + "end" => %{"character" => 12, "line" => 5}, + "start" => %{"character" => 12, "line" => 5} }, selectionRange: %{ - end: %{character: 12, line: 5}, - start: %{character: 12, line: 5} + "end" => %{"character" => 12, "line" => 5}, + "start" => %{"character" => 12, "line" => 5} } } ], kind: 2, name: "MyOtherModule", range: %{ - end: %{character: 6, line: 4}, - start: %{character: 6, line: 4} + "end" => %{"character" => 6, "line" => 4}, + "start" => %{"character" => 6, "line" => 4} }, selectionRange: %{ - end: %{character: 6, line: 4}, - start: %{character: 6, line: 4} + "end" => %{"character" => 6, "line" => 4}, + "start" => %{"character" => 6, "line" => 4} } } ]} = DocumentSymbols.symbols(uri, text, true) @@ -494,8 +575,8 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do kind: 2, location: %{ range: %{ - end: %{character: 6, line: 1}, - start: %{character: 6, line: 1} + "end" => %{"character" => 6, "line" => 1}, + "start" => %{"character" => 6, "line" => 1} } } }, @@ -504,8 +585,8 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do kind: 12, location: %{ range: %{ - end: %{character: 12, line: 2}, - start: %{character: 12, line: 2} + "end" => %{"character" => 12, "line" => 2}, + "start" => %{"character" => 12, "line" => 2} } }, containerName: "MyModule" @@ -515,8 +596,8 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do kind: 2, location: %{ range: %{ - end: %{character: 6, line: 4}, - start: %{character: 6, line: 4} + "end" => %{"character" => 6, "line" => 4}, + "start" => %{"character" => 6, "line" => 4} } } }, @@ -525,8 +606,8 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do name: "def some_other_function()", location: %{ range: %{ - end: %{character: 12, line: 5}, - start: %{character: 12, line: 5} + "end" => %{"character" => 12, "line" => 5}, + "start" => %{"character" => 12, "line" => 5} } }, containerName: "MyOtherModule" @@ -550,17 +631,26 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do children: [], kind: 12, name: "def my_fn()", - range: %{end: %{character: 12, line: 2}, start: %{character: 12, line: 2}}, + range: %{ + "end" => %{"character" => 12, "line" => 2}, + "start" => %{"character" => 12, "line" => 2} + }, selectionRange: %{ - end: %{character: 12, line: 2}, - start: %{character: 12, line: 2} + "end" => %{"character" => 12, "line" => 2}, + "start" => %{"character" => 12, "line" => 2} } } ], kind: 2, name: "MyModule", - range: %{end: %{character: 6, line: 1}, start: %{character: 6, line: 1}}, - selectionRange: %{end: %{character: 6, line: 1}, start: %{character: 6, line: 1}} + range: %{ + "end" => %{"character" => 6, "line" => 1}, + "start" => %{"character" => 6, "line" => 1} + }, + selectionRange: %{ + "end" => %{"character" => 6, "line" => 1}, + "start" => %{"character" => 6, "line" => 1} + } } ]} = DocumentSymbols.symbols(uri, text, true) end @@ -579,14 +669,20 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do name: "MyModule", kind: 2, location: %{ - range: %{end: %{character: 6, line: 1}, start: %{character: 6, line: 1}} + range: %{ + "end" => %{"character" => 6, "line" => 1}, + "start" => %{"character" => 6, "line" => 1} + } } }, %Protocol.SymbolInformation{ name: "def my_fn()", kind: 12, location: %{ - range: %{end: %{character: 12, line: 2}, start: %{character: 12, line: 2}} + range: %{ + "end" => %{"character" => 12, "line" => 2}, + "start" => %{"character" => 12, "line" => 2} + } }, containerName: "MyModule" } @@ -609,17 +705,26 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do children: [], kind: 12, name: "def my_fn()", - range: %{end: %{character: 12, line: 2}, start: %{character: 12, line: 2}}, + range: %{ + "end" => %{"character" => 12, "line" => 2}, + "start" => %{"character" => 12, "line" => 2} + }, selectionRange: %{ - end: %{character: 12, line: 2}, - start: %{character: 12, line: 2} + "end" => %{"character" => 12, "line" => 2}, + "start" => %{"character" => 12, "line" => 2} } } ], kind: 2, name: "# unknown", - range: %{end: %{character: 6, line: 1}, start: %{character: 6, line: 1}}, - selectionRange: %{end: %{character: 6, line: 1}, start: %{character: 6, line: 1}} + range: %{ + "end" => %{"character" => 6, "line" => 1}, + "start" => %{"character" => 6, "line" => 1} + }, + selectionRange: %{ + "end" => %{"character" => 6, "line" => 1}, + "start" => %{"character" => 6, "line" => 1} + } } ]} = DocumentSymbols.symbols(uri, text, true) end @@ -638,14 +743,20 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do name: "# unknown", kind: 2, location: %{ - range: %{end: %{character: 6, line: 1}, start: %{character: 6, line: 1}} + range: %{ + "end" => %{"character" => 6, "line" => 1}, + "start" => %{"character" => 6, "line" => 1} + } } }, %Protocol.SymbolInformation{ kind: 12, name: "def my_fn()", location: %{ - range: %{end: %{character: 12, line: 2}, start: %{character: 12, line: 2}} + range: %{ + "end" => %{"character" => 12, "line" => 2}, + "start" => %{"character" => 12, "line" => 2} + } }, containerName: "# unknown" } @@ -668,17 +779,26 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do children: [], kind: 12, name: "def my_fn()", - range: %{end: %{character: 12, line: 2}, start: %{character: 12, line: 2}}, + range: %{ + "end" => %{"character" => 12, "line" => 2}, + "start" => %{"character" => 12, "line" => 2} + }, selectionRange: %{ - end: %{character: 12, line: 2}, - start: %{character: 12, line: 2} + "end" => %{"character" => 12, "line" => 2}, + "start" => %{"character" => 12, "line" => 2} } } ], kind: 2, name: "my_module", - range: %{end: %{character: 6, line: 1}, start: %{character: 6, line: 1}}, - selectionRange: %{end: %{character: 6, line: 1}, start: %{character: 6, line: 1}} + range: %{ + "end" => %{"character" => 6, "line" => 1}, + "start" => %{"character" => 6, "line" => 1} + }, + selectionRange: %{ + "end" => %{"character" => 6, "line" => 1}, + "start" => %{"character" => 6, "line" => 1} + } } ]} = DocumentSymbols.symbols(uri, text, true) end @@ -697,14 +817,20 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do name: "my_module", kind: 2, location: %{ - range: %{end: %{character: 6, line: 1}, start: %{character: 6, line: 1}} + range: %{ + "end" => %{"character" => 6, "line" => 1}, + "start" => %{"character" => 6, "line" => 1} + } } }, %Protocol.SymbolInformation{ name: "def my_fn()", kind: 12, location: %{ - range: %{end: %{character: 12, line: 2}, start: %{character: 12, line: 2}} + range: %{ + "end" => %{"character" => 12, "line" => 2}, + "start" => %{"character" => 12, "line" => 2} + } }, containerName: "my_module" } @@ -731,26 +857,38 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do children: [], kind: 12, name: "def my_fn()", - range: %{end: %{character: 14, line: 3}, start: %{character: 14, line: 3}}, + range: %{ + "end" => %{"character" => 14, "line" => 3}, + "start" => %{"character" => 14, "line" => 3} + }, selectionRange: %{ - end: %{character: 14, line: 3}, - start: %{character: 14, line: 3} + "end" => %{"character" => 14, "line" => 3}, + "start" => %{"character" => 14, "line" => 3} } } ], kind: 2, name: "__MODULE__.SubModule", - range: %{end: %{character: 8, line: 2}, start: %{character: 8, line: 2}}, + range: %{ + "end" => %{"character" => 8, "line" => 2}, + "start" => %{"character" => 8, "line" => 2} + }, selectionRange: %{ - end: %{character: 8, line: 2}, - start: %{character: 8, line: 2} + "end" => %{"character" => 8, "line" => 2}, + "start" => %{"character" => 8, "line" => 2} } } ], kind: 2, name: "__MODULE__", - range: %{end: %{character: 6, line: 1}, start: %{character: 6, line: 1}}, - selectionRange: %{end: %{character: 6, line: 1}, start: %{character: 6, line: 1}} + range: %{ + "end" => %{"character" => 6, "line" => 1}, + "start" => %{"character" => 6, "line" => 1} + }, + selectionRange: %{ + "end" => %{"character" => 6, "line" => 1}, + "start" => %{"character" => 6, "line" => 1} + } } ]} = DocumentSymbols.symbols(uri, text, true) end @@ -771,14 +909,20 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do name: "__MODULE__", kind: 2, location: %{ - range: %{end: %{character: 6, line: 1}, start: %{character: 6, line: 1}} + range: %{ + "end" => %{"character" => 6, "line" => 1}, + "start" => %{"character" => 6, "line" => 1} + } } }, %Protocol.SymbolInformation{ name: "__MODULE__.SubModule", kind: 2, location: %{ - range: %{end: %{character: 8, line: 2}, start: %{character: 8, line: 2}} + range: %{ + "end" => %{"character" => 8, "line" => 2}, + "start" => %{"character" => 8, "line" => 2} + } }, containerName: "__MODULE__" }, @@ -786,7 +930,10 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do name: "def my_fn()", kind: 12, location: %{ - range: %{end: %{character: 14, line: 3}, start: %{character: 14, line: 3}} + range: %{ + "end" => %{"character" => 14, "line" => 3}, + "start" => %{"character" => 14, "line" => 3} + } }, containerName: "__MODULE__.SubModule" } @@ -819,17 +966,26 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do children: [], kind: 12, name: "def size(data)", - range: %{end: %{character: 6, line: 2}, start: %{character: 6, line: 2}}, + range: %{ + "end" => %{"character" => 6, "line" => 2}, + "start" => %{"character" => 6, "line" => 2} + }, selectionRange: %{ - end: %{character: 6, line: 2}, - start: %{character: 6, line: 2} + "end" => %{"character" => 6, "line" => 2}, + "start" => %{"character" => 6, "line" => 2} } } ], kind: 11, name: "MyProtocol", - range: %{end: %{character: 0, line: 0}, start: %{character: 0, line: 0}}, - selectionRange: %{end: %{character: 0, line: 0}, start: %{character: 0, line: 0}} + range: %{ + "end" => %{"character" => 0, "line" => 0}, + "start" => %{"character" => 0, "line" => 0} + }, + selectionRange: %{ + "end" => %{"character" => 0, "line" => 0}, + "start" => %{"character" => 0, "line" => 0} + } }, %Protocol.DocumentSymbol{ children: [ @@ -837,17 +993,26 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do children: [], kind: 12, name: "def size(binary)", - range: %{end: %{character: 6, line: 6}, start: %{character: 6, line: 6}}, + range: %{ + "end" => %{"character" => 6, "line" => 6}, + "start" => %{"character" => 6, "line" => 6} + }, selectionRange: %{ - end: %{character: 6, line: 6}, - start: %{character: 6, line: 6} + "end" => %{"character" => 6, "line" => 6}, + "start" => %{"character" => 6, "line" => 6} } } ], kind: 2, name: "MyProtocol, for: BitString", - range: %{end: %{character: 0, line: 5}, start: %{character: 0, line: 5}}, - selectionRange: %{end: %{character: 0, line: 5}, start: %{character: 0, line: 5}} + range: %{ + "end" => %{"character" => 0, "line" => 5}, + "start" => %{"character" => 0, "line" => 5} + }, + selectionRange: %{ + "end" => %{"character" => 0, "line" => 5}, + "start" => %{"character" => 0, "line" => 5} + } }, %Protocol.DocumentSymbol{ children: [ @@ -855,17 +1020,26 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do children: [], kind: 12, name: "def size(param)", - range: %{end: %{character: 6, line: 10}, start: %{character: 6, line: 10}}, + range: %{ + "end" => %{"character" => 6, "line" => 10}, + "start" => %{"character" => 6, "line" => 10} + }, selectionRange: %{ - end: %{character: 6, line: 10}, - start: %{character: 6, line: 10} + "end" => %{"character" => 6, "line" => 10}, + "start" => %{"character" => 6, "line" => 10} } } ], kind: 2, name: "MyProtocol, for: [List, MyList]", - range: %{end: %{character: 0, line: 9}, start: %{character: 0, line: 9}}, - selectionRange: %{end: %{character: 0, line: 9}, start: %{character: 0, line: 9}} + range: %{ + "end" => %{"character" => 0, "line" => 9}, + "start" => %{"character" => 0, "line" => 9} + }, + selectionRange: %{ + "end" => %{"character" => 0, "line" => 9}, + "start" => %{"character" => 0, "line" => 9} + } } ]} = DocumentSymbols.symbols(uri, text, true) end @@ -894,14 +1068,20 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do name: "MyProtocol", kind: 11, location: %{ - range: %{end: %{character: 0, line: 0}, start: %{character: 0, line: 0}} + range: %{ + "end" => %{"character" => 0, "line" => 0}, + "start" => %{"character" => 0, "line" => 0} + } } }, %Protocol.SymbolInformation{ kind: 12, name: "def size(data)", location: %{ - range: %{end: %{character: 6, line: 2}, start: %{character: 6, line: 2}} + range: %{ + "end" => %{"character" => 6, "line" => 2}, + "start" => %{"character" => 6, "line" => 2} + } }, containerName: "MyProtocol" }, @@ -909,14 +1089,20 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do kind: 2, name: "MyProtocol, for: BitString", location: %{ - range: %{end: %{character: 0, line: 5}, start: %{character: 0, line: 5}} + range: %{ + "end" => %{"character" => 0, "line" => 5}, + "start" => %{"character" => 0, "line" => 5} + } } }, %Protocol.SymbolInformation{ kind: 12, name: "def size(binary)", location: %{ - range: %{end: %{character: 6, line: 6}, start: %{character: 6, line: 6}} + range: %{ + "end" => %{"character" => 6, "line" => 6}, + "start" => %{"character" => 6, "line" => 6} + } }, containerName: "MyProtocol, for: BitString" }, @@ -924,14 +1110,20 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do kind: 2, name: "MyProtocol, for: [List, MyList]", location: %{ - range: %{end: %{character: 0, line: 9}, start: %{character: 0, line: 9}} + range: %{ + "end" => %{"character" => 0, "line" => 9}, + "start" => %{"character" => 0, "line" => 9} + } } }, %Protocol.SymbolInformation{ kind: 12, name: "def size(param)", location: %{ - range: %{end: %{character: 6, line: 10}, start: %{character: 6, line: 10}} + range: %{ + "end" => %{"character" => 6, "line" => 10}, + "start" => %{"character" => 6, "line" => 10} + } }, containerName: "MyProtocol, for: [List, MyList]" } @@ -957,36 +1149,51 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do children: [], kind: 7, name: "prop", - range: %{end: %{character: 2, line: 1}, start: %{character: 2, line: 1}}, + range: %{ + "end" => %{"character" => 2, "line" => 1}, + "start" => %{"character" => 2, "line" => 1} + }, selectionRange: %{ - end: %{character: 2, line: 1}, - start: %{character: 2, line: 1} + "end" => %{"character" => 2, "line" => 1}, + "start" => %{"character" => 2, "line" => 1} } }, %Protocol.DocumentSymbol{ children: [], kind: 7, name: "prop_with_def", - range: %{end: %{character: 2, line: 1}, start: %{character: 2, line: 1}}, + range: %{ + "end" => %{"character" => 2, "line" => 1}, + "start" => %{"character" => 2, "line" => 1} + }, selectionRange: %{ - end: %{character: 2, line: 1}, - start: %{character: 2, line: 1} + "end" => %{"character" => 2, "line" => 1}, + "start" => %{"character" => 2, "line" => 1} } } ], kind: 23, name: "struct", - range: %{end: %{character: 2, line: 1}, start: %{character: 2, line: 1}}, + range: %{ + "end" => %{"character" => 2, "line" => 1}, + "start" => %{"character" => 2, "line" => 1} + }, selectionRange: %{ - end: %{character: 2, line: 1}, - start: %{character: 2, line: 1} + "end" => %{"character" => 2, "line" => 1}, + "start" => %{"character" => 2, "line" => 1} } } ], kind: 2, name: "MyModule", - range: %{end: %{character: 0, line: 0}, start: %{character: 0, line: 0}}, - selectionRange: %{end: %{character: 0, line: 0}, start: %{character: 0, line: 0}} + range: %{ + "end" => %{"character" => 0, "line" => 0}, + "start" => %{"character" => 0, "line" => 0} + }, + selectionRange: %{ + "end" => %{"character" => 0, "line" => 0}, + "start" => %{"character" => 0, "line" => 0} + } } ]} = DocumentSymbols.symbols(uri, text, true) end @@ -1006,14 +1213,20 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do name: "MyModule", kind: 2, location: %{ - range: %{end: %{character: 0, line: 0}, start: %{character: 0, line: 0}} + range: %{ + "end" => %{"character" => 0, "line" => 0}, + "start" => %{"character" => 0, "line" => 0} + } } }, %Protocol.SymbolInformation{ name: "struct", kind: 23, location: %{ - range: %{end: %{character: 2, line: 1}, start: %{character: 2, line: 1}} + range: %{ + "end" => %{"character" => 2, "line" => 1}, + "start" => %{"character" => 2, "line" => 1} + } }, containerName: "MyModule" }, @@ -1021,7 +1234,10 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do name: "prop", kind: 7, location: %{ - range: %{end: %{character: 2, line: 1}, start: %{character: 2, line: 1}} + range: %{ + "end" => %{"character" => 2, "line" => 1}, + "start" => %{"character" => 2, "line" => 1} + } }, containerName: "struct" }, @@ -1029,7 +1245,10 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do kind: 7, name: "prop_with_def", location: %{ - range: %{end: %{character: 2, line: 1}, start: %{character: 2, line: 1}} + range: %{ + "end" => %{"character" => 2, "line" => 1}, + "start" => %{"character" => 2, "line" => 1} + } }, containerName: "struct" } @@ -1055,26 +1274,38 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do children: [], kind: 7, name: "message", - range: %{end: %{character: 2, line: 1}, start: %{character: 2, line: 1}}, + range: %{ + "end" => %{"character" => 2, "line" => 1}, + "start" => %{"character" => 2, "line" => 1} + }, selectionRange: %{ - end: %{character: 2, line: 1}, - start: %{character: 2, line: 1} + "end" => %{"character" => 2, "line" => 1}, + "start" => %{"character" => 2, "line" => 1} } } ], kind: 23, name: "exception", - range: %{end: %{character: 2, line: 1}, start: %{character: 2, line: 1}}, + range: %{ + "end" => %{"character" => 2, "line" => 1}, + "start" => %{"character" => 2, "line" => 1} + }, selectionRange: %{ - end: %{character: 2, line: 1}, - start: %{character: 2, line: 1} + "end" => %{"character" => 2, "line" => 1}, + "start" => %{"character" => 2, "line" => 1} } } ], kind: 2, name: "MyError", - range: %{end: %{character: 0, line: 0}, start: %{character: 0, line: 0}}, - selectionRange: %{end: %{character: 0, line: 0}, start: %{character: 0, line: 0}} + range: %{ + "end" => %{"character" => 0, "line" => 0}, + "start" => %{"character" => 0, "line" => 0} + }, + selectionRange: %{ + "end" => %{"character" => 0, "line" => 0}, + "start" => %{"character" => 0, "line" => 0} + } } ]} = DocumentSymbols.symbols(uri, text, true) end @@ -1094,14 +1325,20 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do name: "MyError", kind: 2, location: %{ - range: %{end: %{character: 0, line: 0}, start: %{character: 0, line: 0}} + range: %{ + "end" => %{"character" => 0, "line" => 0}, + "start" => %{"character" => 0, "line" => 0} + } } }, %Protocol.SymbolInformation{ kind: 23, name: "exception", location: %{ - range: %{end: %{character: 2, line: 1}, start: %{character: 2, line: 1}} + range: %{ + "end" => %{"character" => 2, "line" => 1}, + "start" => %{"character" => 2, "line" => 1} + } }, containerName: "MyError" }, @@ -1109,7 +1346,10 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do kind: 7, name: "message", location: %{ - range: %{end: %{character: 2, line: 1}, start: %{character: 2, line: 1}} + range: %{ + "end" => %{"character" => 2, "line" => 1}, + "start" => %{"character" => 2, "line" => 1} + } }, containerName: "exception" } @@ -1138,67 +1378,91 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do children: [], kind: 5, name: "my_simple", - range: %{end: %{character: 3, line: 1}, start: %{character: 3, line: 1}}, + range: %{ + "end" => %{"character" => 3, "line" => 1}, + "start" => %{"character" => 3, "line" => 1} + }, selectionRange: %{ - end: %{character: 3, line: 1}, - start: %{character: 3, line: 1} + "end" => %{"character" => 3, "line" => 1}, + "start" => %{"character" => 3, "line" => 1} } }, %Protocol.DocumentSymbol{ children: [], kind: 5, name: "my_union", - range: %{end: %{character: 3, line: 2}, start: %{character: 3, line: 2}}, + range: %{ + "end" => %{"character" => 3, "line" => 2}, + "start" => %{"character" => 3, "line" => 2} + }, selectionRange: %{ - end: %{character: 3, line: 2}, - start: %{character: 3, line: 2} + "end" => %{"character" => 3, "line" => 2}, + "start" => %{"character" => 3, "line" => 2} } }, %Protocol.DocumentSymbol{ children: [], kind: 5, name: "my_simple_private", - range: %{end: %{character: 3, line: 3}, start: %{character: 3, line: 3}}, + range: %{ + "end" => %{"character" => 3, "line" => 3}, + "start" => %{"character" => 3, "line" => 3} + }, selectionRange: %{ - end: %{character: 3, line: 3}, - start: %{character: 3, line: 3} + "end" => %{"character" => 3, "line" => 3}, + "start" => %{"character" => 3, "line" => 3} } }, %Protocol.DocumentSymbol{ children: [], kind: 5, name: "my_simple_opaque", - range: %{end: %{character: 3, line: 4}, start: %{character: 3, line: 4}}, + range: %{ + "end" => %{"character" => 3, "line" => 4}, + "start" => %{"character" => 3, "line" => 4} + }, selectionRange: %{ - end: %{character: 3, line: 4}, - start: %{character: 3, line: 4} + "end" => %{"character" => 3, "line" => 4}, + "start" => %{"character" => 3, "line" => 4} } }, %Protocol.DocumentSymbol{ children: [], kind: 5, name: "my_with_args(key, value)", - range: %{end: %{character: 3, line: 5}, start: %{character: 3, line: 5}}, + range: %{ + "end" => %{"character" => 3, "line" => 5}, + "start" => %{"character" => 3, "line" => 5} + }, selectionRange: %{ - end: %{character: 3, line: 5}, - start: %{character: 3, line: 5} + "end" => %{"character" => 3, "line" => 5}, + "start" => %{"character" => 3, "line" => 5} } }, %Protocol.DocumentSymbol{ children: [], kind: 5, name: "my_with_args_when(key, value)", - range: %{end: %{character: 3, line: 6}, start: %{character: 3, line: 6}}, + range: %{ + "end" => %{"character" => 3, "line" => 6}, + "start" => %{"character" => 3, "line" => 6} + }, selectionRange: %{ - end: %{character: 3, line: 6}, - start: %{character: 3, line: 6} + "end" => %{"character" => 3, "line" => 6}, + "start" => %{"character" => 3, "line" => 6} } } ], kind: 2, name: "MyModule", - range: %{end: %{character: 0, line: 0}, start: %{character: 0, line: 0}}, - selectionRange: %{end: %{character: 0, line: 0}, start: %{character: 0, line: 0}} + range: %{ + "end" => %{"character" => 0, "line" => 0}, + "start" => %{"character" => 0, "line" => 0} + }, + selectionRange: %{ + "end" => %{"character" => 0, "line" => 0}, + "start" => %{"character" => 0, "line" => 0} + } } ]} = DocumentSymbols.symbols(uri, text, true) end @@ -1223,14 +1487,20 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do name: "MyModule", kind: 2, location: %{ - range: %{end: %{character: 0, line: 0}, start: %{character: 0, line: 0}} + range: %{ + "end" => %{"character" => 0, "line" => 0}, + "start" => %{"character" => 0, "line" => 0} + } } }, %Protocol.SymbolInformation{ kind: 5, name: "my_simple", location: %{ - range: %{end: %{character: 3, line: 1}, start: %{character: 3, line: 1}} + range: %{ + "end" => %{"character" => 3, "line" => 1}, + "start" => %{"character" => 3, "line" => 1} + } }, containerName: "MyModule" }, @@ -1238,7 +1508,10 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do kind: 5, name: "my_union", location: %{ - range: %{end: %{character: 3, line: 2}, start: %{character: 3, line: 2}} + range: %{ + "end" => %{"character" => 3, "line" => 2}, + "start" => %{"character" => 3, "line" => 2} + } }, containerName: "MyModule" }, @@ -1246,7 +1519,10 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do kind: 5, name: "my_simple_private", location: %{ - range: %{end: %{character: 3, line: 3}, start: %{character: 3, line: 3}} + range: %{ + "end" => %{"character" => 3, "line" => 3}, + "start" => %{"character" => 3, "line" => 3} + } }, containerName: "MyModule" }, @@ -1254,7 +1530,10 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do kind: 5, name: "my_simple_opaque", location: %{ - range: %{end: %{character: 3, line: 4}, start: %{character: 3, line: 4}} + range: %{ + "end" => %{"character" => 3, "line" => 4}, + "start" => %{"character" => 3, "line" => 4} + } }, containerName: "MyModule" }, @@ -1262,7 +1541,10 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do kind: 5, name: "my_with_args(key, value)", location: %{ - range: %{end: %{character: 3, line: 5}, start: %{character: 3, line: 5}} + range: %{ + "end" => %{"character" => 3, "line" => 5}, + "start" => %{"character" => 3, "line" => 5} + } }, containerName: "MyModule" }, @@ -1270,7 +1552,10 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do kind: 5, name: "my_with_args_when(key, value)", location: %{ - range: %{end: %{character: 3, line: 6}, start: %{character: 3, line: 6}} + range: %{ + "end" => %{"character" => 3, "line" => 6}, + "start" => %{"character" => 3, "line" => 6} + } }, containerName: "MyModule" } @@ -1301,67 +1586,91 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do children: [], kind: 24, name: "my_callback(type1, type2)", - range: %{end: %{character: 3, line: 1}, start: %{character: 3, line: 1}}, + range: %{ + "end" => %{"character" => 3, "line" => 1}, + "start" => %{"character" => 3, "line" => 1} + }, selectionRange: %{ - end: %{character: 3, line: 1}, - start: %{character: 3, line: 1} + "end" => %{"character" => 3, "line" => 1}, + "start" => %{"character" => 3, "line" => 1} } }, %Protocol.DocumentSymbol{ children: [], kind: 24, name: "my_macrocallback(type1, type2)", - range: %{end: %{character: 3, line: 2}, start: %{character: 3, line: 2}}, + range: %{ + "end" => %{"character" => 3, "line" => 2}, + "start" => %{"character" => 3, "line" => 2} + }, selectionRange: %{ - end: %{character: 3, line: 2}, - start: %{character: 3, line: 2} + "end" => %{"character" => 3, "line" => 2}, + "start" => %{"character" => 3, "line" => 2} } }, %Protocol.DocumentSymbol{ children: [], kind: 24, name: "my_callback_when(type1, type2)", - range: %{end: %{character: 3, line: 4}, start: %{character: 3, line: 4}}, + range: %{ + "end" => %{"character" => 3, "line" => 4}, + "start" => %{"character" => 3, "line" => 4} + }, selectionRange: %{ - end: %{character: 3, line: 4}, - start: %{character: 3, line: 4} + "end" => %{"character" => 3, "line" => 4}, + "start" => %{"character" => 3, "line" => 4} } }, %Protocol.DocumentSymbol{ children: [], kind: 24, name: "my_macrocallback_when(type1, type2)", - range: %{end: %{character: 3, line: 5}, start: %{character: 3, line: 5}}, + range: %{ + "end" => %{"character" => 3, "line" => 5}, + "start" => %{"character" => 3, "line" => 5} + }, selectionRange: %{ - end: %{character: 3, line: 5}, - start: %{character: 3, line: 5} + "end" => %{"character" => 3, "line" => 5}, + "start" => %{"character" => 3, "line" => 5} } }, %Protocol.DocumentSymbol{ children: [], kind: 24, name: "my_callback_no_arg()", - range: %{end: %{character: 3, line: 7}, start: %{character: 3, line: 7}}, + range: %{ + "end" => %{"character" => 3, "line" => 7}, + "start" => %{"character" => 3, "line" => 7} + }, selectionRange: %{ - end: %{character: 3, line: 7}, - start: %{character: 3, line: 7} + "end" => %{"character" => 3, "line" => 7}, + "start" => %{"character" => 3, "line" => 7} } }, %Protocol.DocumentSymbol{ children: [], kind: 24, name: "my_macrocallback_no_arg()", - range: %{end: %{character: 3, line: 8}, start: %{character: 3, line: 8}}, + range: %{ + "end" => %{"character" => 3, "line" => 8}, + "start" => %{"character" => 3, "line" => 8} + }, selectionRange: %{ - end: %{character: 3, line: 8}, - start: %{character: 3, line: 8} + "end" => %{"character" => 3, "line" => 8}, + "start" => %{"character" => 3, "line" => 8} } } ], kind: 2, name: "MyModule", - range: %{end: %{character: 0, line: 0}, start: %{character: 0, line: 0}}, - selectionRange: %{end: %{character: 0, line: 0}, start: %{character: 0, line: 0}} + range: %{ + "end" => %{"character" => 0, "line" => 0}, + "start" => %{"character" => 0, "line" => 0} + }, + selectionRange: %{ + "end" => %{"character" => 0, "line" => 0}, + "start" => %{"character" => 0, "line" => 0} + } } ]} = DocumentSymbols.symbols(uri, text, true) end @@ -1388,14 +1697,20 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do name: "MyModule", kind: 2, location: %{ - range: %{end: %{character: 0, line: 0}, start: %{character: 0, line: 0}} + range: %{ + "end" => %{"character" => 0, "line" => 0}, + "start" => %{"character" => 0, "line" => 0} + } } }, %Protocol.SymbolInformation{ name: "my_callback(type1, type2)", kind: 24, location: %{ - range: %{end: %{character: 3, line: 1}, start: %{character: 3, line: 1}} + range: %{ + "end" => %{"character" => 3, "line" => 1}, + "start" => %{"character" => 3, "line" => 1} + } }, containerName: "MyModule" }, @@ -1403,7 +1718,10 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do name: "my_macrocallback(type1, type2)", kind: 24, location: %{ - range: %{end: %{character: 3, line: 2}, start: %{character: 3, line: 2}} + range: %{ + "end" => %{"character" => 3, "line" => 2}, + "start" => %{"character" => 3, "line" => 2} + } }, containerName: "MyModule" }, @@ -1411,7 +1729,10 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do name: "my_callback_when(type1, type2)", kind: 24, location: %{ - range: %{end: %{character: 3, line: 4}, start: %{character: 3, line: 4}} + range: %{ + "end" => %{"character" => 3, "line" => 4}, + "start" => %{"character" => 3, "line" => 4} + } }, containerName: "MyModule" }, @@ -1419,7 +1740,10 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do name: "my_macrocallback_when(type1, type2)", kind: 24, location: %{ - range: %{end: %{character: 3, line: 5}, start: %{character: 3, line: 5}} + range: %{ + "end" => %{"character" => 3, "line" => 5}, + "start" => %{"character" => 3, "line" => 5} + } }, containerName: "MyModule" }, @@ -1427,7 +1751,10 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do name: "my_callback_no_arg()", kind: 24, location: %{ - range: %{end: %{character: 3, line: 7}, start: %{character: 3, line: 7}} + range: %{ + "end" => %{"character" => 3, "line" => 7}, + "start" => %{"character" => 3, "line" => 7} + } }, containerName: "MyModule" }, @@ -1435,7 +1762,10 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do name: "my_macrocallback_no_arg()", kind: 24, location: %{ - range: %{end: %{character: 3, line: 8}, start: %{character: 3, line: 8}} + range: %{ + "end" => %{"character" => 3, "line" => 8}, + "start" => %{"character" => 3, "line" => 8} + } }, containerName: "MyModule" } @@ -1459,27 +1789,39 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do children: [], kind: 24, name: "my_fn(integer)", - range: %{end: %{character: 9, line: 2}, start: %{character: 9, line: 2}}, + range: %{ + "end" => %{"character" => 9, "line" => 2}, + "start" => %{"character" => 9, "line" => 2} + }, selectionRange: %{ - end: %{character: 9, line: 2}, - start: %{character: 9, line: 2} + "end" => %{"character" => 9, "line" => 2}, + "start" => %{"character" => 9, "line" => 2} } }, %Protocol.DocumentSymbol{ children: [], kind: 12, name: "def my_fn(a)", - range: %{end: %{character: 12, line: 3}, start: %{character: 12, line: 3}}, + range: %{ + "end" => %{"character" => 12, "line" => 3}, + "start" => %{"character" => 12, "line" => 3} + }, selectionRange: %{ - end: %{character: 12, line: 3}, - start: %{character: 12, line: 3} + "end" => %{"character" => 12, "line" => 3}, + "start" => %{"character" => 12, "line" => 3} } } ], kind: 2, name: "MyModule", - range: %{end: %{character: 6, line: 1}, start: %{character: 6, line: 1}}, - selectionRange: %{end: %{character: 6, line: 1}, start: %{character: 6, line: 1}} + range: %{ + "end" => %{"character" => 6, "line" => 1}, + "start" => %{"character" => 6, "line" => 1} + }, + selectionRange: %{ + "end" => %{"character" => 6, "line" => 1}, + "start" => %{"character" => 6, "line" => 1} + } } ]} = DocumentSymbols.symbols(uri, text, true) end @@ -1499,14 +1841,20 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do name: "MyModule", kind: 2, location: %{ - range: %{end: %{character: 6, line: 1}, start: %{character: 6, line: 1}} + range: %{ + "end" => %{"character" => 6, "line" => 1}, + "start" => %{"character" => 6, "line" => 1} + } } }, %Protocol.SymbolInformation{ name: "my_fn(integer)", kind: 24, location: %{ - range: %{end: %{character: 9, line: 2}, start: %{character: 9, line: 2}} + range: %{ + "end" => %{"character" => 9, "line" => 2}, + "start" => %{"character" => 9, "line" => 2} + } }, containerName: "MyModule" }, @@ -1514,7 +1862,10 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do name: "def my_fn(a)", kind: 12, location: %{ - range: %{end: %{character: 12, line: 3}, start: %{character: 12, line: 3}} + range: %{ + "end" => %{"character" => 12, "line" => 3}, + "start" => %{"character" => 12, "line" => 3} + } }, containerName: "MyModule" } @@ -1538,8 +1889,14 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do children: [], kind: 2, name: "MyModule", - range: %{end: %{character: 0, line: 0}, start: %{character: 0, line: 0}}, - selectionRange: %{end: %{character: 0, line: 0}, start: %{character: 0, line: 0}} + range: %{ + "end" => %{"character" => 0, "line" => 0}, + "start" => %{"character" => 0, "line" => 0} + }, + selectionRange: %{ + "end" => %{"character" => 0, "line" => 0}, + "start" => %{"character" => 0, "line" => 0} + } } ]} = DocumentSymbols.symbols(uri, text, true) end @@ -1561,7 +1918,10 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do name: "MyModule", kind: 2, location: %{ - range: %{end: %{character: 0, line: 0}, start: %{character: 0, line: 0}} + range: %{ + "end" => %{"character" => 0, "line" => 0}, + "start" => %{"character" => 0, "line" => 0} + } } } ]} = DocumentSymbols.symbols(uri, text, false) @@ -1600,177 +1960,234 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do children: [], kind: 14, name: "@optional_callbacks", - range: %{end: %{character: 3, line: 1}, start: %{character: 3, line: 1}}, + range: %{ + "end" => %{"character" => 3, "line" => 1}, + "start" => %{"character" => 3, "line" => 1} + }, selectionRange: %{ - end: %{character: 3, line: 1}, - start: %{character: 3, line: 1} + "end" => %{"character" => 3, "line" => 1}, + "start" => %{"character" => 3, "line" => 1} } }, %Protocol.DocumentSymbol{ children: [], kind: 14, name: "@behaviour MyBehaviour", - range: %{end: %{character: 3, line: 2}, start: %{character: 3, line: 2}}, + range: %{ + "end" => %{"character" => 3, "line" => 2}, + "start" => %{"character" => 3, "line" => 2} + }, selectionRange: %{ - end: %{character: 3, line: 2}, - start: %{character: 3, line: 2} + "end" => %{"character" => 3, "line" => 2}, + "start" => %{"character" => 3, "line" => 2} } }, %Protocol.DocumentSymbol{ children: [], kind: 14, name: "@impl true", - range: %{end: %{character: 3, line: 3}, start: %{character: 3, line: 3}}, + range: %{ + "end" => %{"character" => 3, "line" => 3}, + "start" => %{"character" => 3, "line" => 3} + }, selectionRange: %{ - end: %{character: 3, line: 3}, - start: %{character: 3, line: 3} + "end" => %{"character" => 3, "line" => 3}, + "start" => %{"character" => 3, "line" => 3} } }, %Protocol.DocumentSymbol{ children: [], kind: 14, name: "@derive", - range: %{end: %{character: 3, line: 4}, start: %{character: 3, line: 4}}, + range: %{ + "end" => %{"character" => 3, "line" => 4}, + "start" => %{"character" => 3, "line" => 4} + }, selectionRange: %{ - end: %{character: 3, line: 4}, - start: %{character: 3, line: 4} + "end" => %{"character" => 3, "line" => 4}, + "start" => %{"character" => 3, "line" => 4} } }, %Protocol.DocumentSymbol{ children: [], kind: 14, name: "@enforce_keys", - range: %{end: %{character: 3, line: 5}, start: %{character: 3, line: 5}}, + range: %{ + "end" => %{"character" => 3, "line" => 5}, + "start" => %{"character" => 3, "line" => 5} + }, selectionRange: %{ - end: %{character: 3, line: 5}, - start: %{character: 3, line: 5} + "end" => %{"character" => 3, "line" => 5}, + "start" => %{"character" => 3, "line" => 5} } }, %Protocol.DocumentSymbol{ children: [], kind: 14, name: "@compile", - range: %{end: %{character: 3, line: 6}, start: %{character: 3, line: 6}}, + range: %{ + "end" => %{"character" => 3, "line" => 6}, + "start" => %{"character" => 3, "line" => 6} + }, selectionRange: %{ - end: %{character: 3, line: 6}, - start: %{character: 3, line: 6} + "end" => %{"character" => 3, "line" => 6}, + "start" => %{"character" => 3, "line" => 6} } }, %Protocol.DocumentSymbol{ children: [], kind: 14, name: "@deprecated", - range: %{end: %{character: 3, line: 7}, start: %{character: 3, line: 7}}, + range: %{ + "end" => %{"character" => 3, "line" => 7}, + "start" => %{"character" => 3, "line" => 7} + }, selectionRange: %{ - end: %{character: 3, line: 7}, - start: %{character: 3, line: 7} + "end" => %{"character" => 3, "line" => 7}, + "start" => %{"character" => 3, "line" => 7} } }, %Protocol.DocumentSymbol{ children: [], kind: 14, name: "@dialyzer", - range: %{end: %{character: 3, line: 8}, start: %{character: 3, line: 8}}, + range: %{ + "end" => %{"character" => 3, "line" => 8}, + "start" => %{"character" => 3, "line" => 8} + }, selectionRange: %{ - end: %{character: 3, line: 8}, - start: %{character: 3, line: 8} + "end" => %{"character" => 3, "line" => 8}, + "start" => %{"character" => 3, "line" => 8} } }, %Protocol.DocumentSymbol{ children: [], kind: 14, name: "@file", - range: %{end: %{character: 3, line: 9}, start: %{character: 3, line: 9}}, + range: %{ + "end" => %{"character" => 3, "line" => 9}, + "start" => %{"character" => 3, "line" => 9} + }, selectionRange: %{ - end: %{character: 3, line: 9}, - start: %{character: 3, line: 9} + "end" => %{"character" => 3, "line" => 9}, + "start" => %{"character" => 3, "line" => 9} } }, %Protocol.DocumentSymbol{ children: [], kind: 14, name: "@external_resource", - range: %{end: %{character: 3, line: 10}, start: %{character: 3, line: 10}}, + range: %{ + "end" => %{"character" => 3, "line" => 10}, + "start" => %{"character" => 3, "line" => 10} + }, selectionRange: %{ - end: %{character: 3, line: 10}, - start: %{character: 3, line: 10} + "end" => %{"character" => 3, "line" => 10}, + "start" => %{"character" => 3, "line" => 10} } }, %Protocol.DocumentSymbol{ children: [], kind: 14, name: "@on_load", - range: %{end: %{character: 3, line: 11}, start: %{character: 3, line: 11}}, + range: %{ + "end" => %{"character" => 3, "line" => 11}, + "start" => %{"character" => 3, "line" => 11} + }, selectionRange: %{ - end: %{character: 3, line: 11}, - start: %{character: 3, line: 11} + "end" => %{"character" => 3, "line" => 11}, + "start" => %{"character" => 3, "line" => 11} } }, %Protocol.DocumentSymbol{ children: [], kind: 14, name: "@on_definition", - range: %{end: %{character: 3, line: 12}, start: %{character: 3, line: 12}}, + range: %{ + "end" => %{"character" => 3, "line" => 12}, + "start" => %{"character" => 3, "line" => 12} + }, selectionRange: %{ - end: %{character: 3, line: 12}, - start: %{character: 3, line: 12} + "end" => %{"character" => 3, "line" => 12}, + "start" => %{"character" => 3, "line" => 12} } }, %Protocol.DocumentSymbol{ children: [], kind: 14, name: "@vsn", - range: %{end: %{character: 3, line: 13}, start: %{character: 3, line: 13}}, + range: %{ + "end" => %{"character" => 3, "line" => 13}, + "start" => %{"character" => 3, "line" => 13} + }, selectionRange: %{ - end: %{character: 3, line: 13}, - start: %{character: 3, line: 13} + "end" => %{"character" => 3, "line" => 13}, + "start" => %{"character" => 3, "line" => 13} } }, %Protocol.DocumentSymbol{ children: [], kind: 14, name: "@after_compile", - range: %{end: %{character: 3, line: 14}, start: %{character: 3, line: 14}}, + range: %{ + "end" => %{"character" => 3, "line" => 14}, + "start" => %{"character" => 3, "line" => 14} + }, selectionRange: %{ - end: %{character: 3, line: 14}, - start: %{character: 3, line: 14} + "end" => %{"character" => 3, "line" => 14}, + "start" => %{"character" => 3, "line" => 14} } }, %Protocol.DocumentSymbol{ children: [], kind: 14, name: "@before_compile", - range: %{end: %{character: 3, line: 15}, start: %{character: 3, line: 15}}, + range: %{ + "end" => %{"character" => 3, "line" => 15}, + "start" => %{"character" => 3, "line" => 15} + }, selectionRange: %{ - end: %{character: 3, line: 15}, - start: %{character: 3, line: 15} + "end" => %{"character" => 3, "line" => 15}, + "start" => %{"character" => 3, "line" => 15} } }, %Protocol.DocumentSymbol{ children: [], kind: 14, name: "@fallback_to_any", - range: %{end: %{character: 3, line: 16}, start: %{character: 3, line: 16}}, + range: %{ + "end" => %{"character" => 3, "line" => 16}, + "start" => %{"character" => 3, "line" => 16} + }, selectionRange: %{ - end: %{character: 3, line: 16}, - start: %{character: 3, line: 16} + "end" => %{"character" => 3, "line" => 16}, + "start" => %{"character" => 3, "line" => 16} } }, %Protocol.DocumentSymbol{ children: [], kind: 14, name: "@impl MyBehaviour", - range: %{end: %{character: 3, line: 17}, start: %{character: 3, line: 17}}, + range: %{ + "end" => %{"character" => 3, "line" => 17}, + "start" => %{"character" => 3, "line" => 17} + }, selectionRange: %{ - end: %{character: 3, line: 17}, - start: %{character: 3, line: 17} + "end" => %{"character" => 3, "line" => 17}, + "start" => %{"character" => 3, "line" => 17} } } ], kind: 2, name: "MyModule", - range: %{end: %{character: 0, line: 0}, start: %{character: 0, line: 0}}, - selectionRange: %{end: %{character: 0, line: 0}, start: %{character: 0, line: 0}} + range: %{ + "end" => %{"character" => 0, "line" => 0}, + "start" => %{"character" => 0, "line" => 0} + }, + selectionRange: %{ + "end" => %{"character" => 0, "line" => 0}, + "start" => %{"character" => 0, "line" => 0} + } } ]} = DocumentSymbols.symbols(uri, text, true) end @@ -1806,14 +2223,20 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do name: "MyModule", kind: 2, location: %{ - range: %{end: %{character: 0, line: 0}, start: %{character: 0, line: 0}} + range: %{ + "end" => %{"character" => 0, "line" => 0}, + "start" => %{"character" => 0, "line" => 0} + } } }, %Protocol.SymbolInformation{ name: "@optional_callbacks", kind: 14, location: %{ - range: %{end: %{character: 3, line: 1}, start: %{character: 3, line: 1}} + range: %{ + "end" => %{"character" => 3, "line" => 1}, + "start" => %{"character" => 3, "line" => 1} + } }, containerName: "MyModule" }, @@ -1821,7 +2244,10 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do name: "@behaviour MyBehaviour", kind: 14, location: %{ - range: %{end: %{character: 3, line: 2}, start: %{character: 3, line: 2}} + range: %{ + "end" => %{"character" => 3, "line" => 2}, + "start" => %{"character" => 3, "line" => 2} + } }, containerName: "MyModule" }, @@ -1829,7 +2255,10 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do name: "@impl true", kind: 14, location: %{ - range: %{end: %{character: 3, line: 3}, start: %{character: 3, line: 3}} + range: %{ + "end" => %{"character" => 3, "line" => 3}, + "start" => %{"character" => 3, "line" => 3} + } }, containerName: "MyModule" }, @@ -1837,7 +2266,10 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do name: "@derive", kind: 14, location: %{ - range: %{end: %{character: 3, line: 4}, start: %{character: 3, line: 4}} + range: %{ + "end" => %{"character" => 3, "line" => 4}, + "start" => %{"character" => 3, "line" => 4} + } }, containerName: "MyModule" }, @@ -1845,7 +2277,10 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do name: "@enforce_keys", kind: 14, location: %{ - range: %{end: %{character: 3, line: 5}, start: %{character: 3, line: 5}} + range: %{ + "end" => %{"character" => 3, "line" => 5}, + "start" => %{"character" => 3, "line" => 5} + } }, containerName: "MyModule" }, @@ -1853,7 +2288,10 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do name: "@compile", kind: 14, location: %{ - range: %{end: %{character: 3, line: 6}, start: %{character: 3, line: 6}} + range: %{ + "end" => %{"character" => 3, "line" => 6}, + "start" => %{"character" => 3, "line" => 6} + } }, containerName: "MyModule" }, @@ -1861,7 +2299,10 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do name: "@deprecated", kind: 14, location: %{ - range: %{end: %{character: 3, line: 7}, start: %{character: 3, line: 7}} + range: %{ + "end" => %{"character" => 3, "line" => 7}, + "start" => %{"character" => 3, "line" => 7} + } }, containerName: "MyModule" }, @@ -1869,7 +2310,10 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do name: "@dialyzer", kind: 14, location: %{ - range: %{end: %{character: 3, line: 8}, start: %{character: 3, line: 8}} + range: %{ + "end" => %{"character" => 3, "line" => 8}, + "start" => %{"character" => 3, "line" => 8} + } }, containerName: "MyModule" }, @@ -1877,7 +2321,10 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do name: "@file", kind: 14, location: %{ - range: %{end: %{character: 3, line: 9}, start: %{character: 3, line: 9}} + range: %{ + "end" => %{"character" => 3, "line" => 9}, + "start" => %{"character" => 3, "line" => 9} + } }, containerName: "MyModule" }, @@ -1885,7 +2332,10 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do name: "@external_resource", kind: 14, location: %{ - range: %{end: %{character: 3, line: 10}, start: %{character: 3, line: 10}} + range: %{ + "end" => %{"character" => 3, "line" => 10}, + "start" => %{"character" => 3, "line" => 10} + } }, containerName: "MyModule" }, @@ -1893,7 +2343,10 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do name: "@on_load", kind: 14, location: %{ - range: %{end: %{character: 3, line: 11}, start: %{character: 3, line: 11}} + range: %{ + "end" => %{"character" => 3, "line" => 11}, + "start" => %{"character" => 3, "line" => 11} + } }, containerName: "MyModule" }, @@ -1901,7 +2354,10 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do name: "@on_definition", kind: 14, location: %{ - range: %{end: %{character: 3, line: 12}, start: %{character: 3, line: 12}} + range: %{ + "end" => %{"character" => 3, "line" => 12}, + "start" => %{"character" => 3, "line" => 12} + } }, containerName: "MyModule" }, @@ -1909,7 +2365,10 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do name: "@vsn", kind: 14, location: %{ - range: %{end: %{character: 3, line: 13}, start: %{character: 3, line: 13}} + range: %{ + "end" => %{"character" => 3, "line" => 13}, + "start" => %{"character" => 3, "line" => 13} + } }, containerName: "MyModule" }, @@ -1917,7 +2376,10 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do name: "@after_compile", kind: 14, location: %{ - range: %{end: %{character: 3, line: 14}, start: %{character: 3, line: 14}} + range: %{ + "end" => %{"character" => 3, "line" => 14}, + "start" => %{"character" => 3, "line" => 14} + } }, containerName: "MyModule" }, @@ -1925,7 +2387,10 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do name: "@before_compile", kind: 14, location: %{ - range: %{end: %{character: 3, line: 15}, start: %{character: 3, line: 15}} + range: %{ + "end" => %{"character" => 3, "line" => 15}, + "start" => %{"character" => 3, "line" => 15} + } }, containerName: "MyModule" }, @@ -1933,7 +2398,10 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do name: "@fallback_to_any", kind: 14, location: %{ - range: %{end: %{character: 3, line: 16}, start: %{character: 3, line: 16}} + range: %{ + "end" => %{"character" => 3, "line" => 16}, + "start" => %{"character" => 3, "line" => 16} + } }, containerName: "MyModule" }, @@ -1941,7 +2409,10 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do name: "@impl MyBehaviour", kind: 14, location: %{ - range: %{end: %{character: 3, line: 17}, start: %{character: 3, line: 17}} + range: %{ + "end" => %{"character" => 3, "line" => 17}, + "start" => %{"character" => 3, "line" => 17} + } }, containerName: "MyModule" } @@ -1966,24 +2437,24 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do kind: 12, name: "test \"does something\"", range: %{ - end: %{character: 8, line: 3}, - start: %{character: 8, line: 3} + "end" => %{"character" => 8, "line" => 3}, + "start" => %{"character" => 8, "line" => 3} }, selectionRange: %{ - end: %{character: 8, line: 3}, - start: %{character: 8, line: 3} + "end" => %{"character" => 8, "line" => 3}, + "start" => %{"character" => 8, "line" => 3} } } ], kind: 2, name: "MyModuleTest", range: %{ - end: %{character: 6, line: 1}, - start: %{character: 6, line: 1} + "end" => %{"character" => 6, "line" => 1}, + "start" => %{"character" => 6, "line" => 1} }, selectionRange: %{ - end: %{character: 6, line: 1}, - start: %{character: 6, line: 1} + "end" => %{"character" => 6, "line" => 1}, + "start" => %{"character" => 6, "line" => 1} } } ]} = DocumentSymbols.symbols(uri, text, true) @@ -2004,14 +2475,20 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do name: "MyModuleTest", kind: 2, location: %{ - range: %{end: %{character: 6, line: 1}, start: %{character: 6, line: 1}} + range: %{ + "end" => %{"character" => 6, "line" => 1}, + "start" => %{"character" => 6, "line" => 1} + } } }, %Protocol.SymbolInformation{ name: "test \"does something\"", kind: 12, location: %{ - range: %{end: %{character: 8, line: 3}, start: %{character: 8, line: 3}} + range: %{ + "end" => %{"character" => 8, "line" => 3}, + "start" => %{"character" => 8, "line" => 3} + } }, containerName: "MyModuleTest" } @@ -2040,36 +2517,36 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do kind: 12, name: "test \"does something\"", range: %{ - end: %{character: 10, line: 4}, - start: %{character: 10, line: 4} + "end" => %{"character" => 10, "line" => 4}, + "start" => %{"character" => 10, "line" => 4} }, selectionRange: %{ - end: %{character: 10, line: 4}, - start: %{character: 10, line: 4} + "end" => %{"character" => 10, "line" => 4}, + "start" => %{"character" => 10, "line" => 4} } } ], kind: 12, name: "describe \"some description\"", range: %{ - end: %{character: 8, line: 3}, - start: %{character: 8, line: 3} + "end" => %{"character" => 8, "line" => 3}, + "start" => %{"character" => 8, "line" => 3} }, selectionRange: %{ - end: %{character: 8, line: 3}, - start: %{character: 8, line: 3} + "end" => %{"character" => 8, "line" => 3}, + "start" => %{"character" => 8, "line" => 3} } } ], kind: 2, name: "MyModuleTest", range: %{ - end: %{character: 6, line: 1}, - start: %{character: 6, line: 1} + "end" => %{"character" => 6, "line" => 1}, + "start" => %{"character" => 6, "line" => 1} }, selectionRange: %{ - end: %{character: 6, line: 1}, - start: %{character: 6, line: 1} + "end" => %{"character" => 6, "line" => 1}, + "start" => %{"character" => 6, "line" => 1} } } ]} = DocumentSymbols.symbols(uri, text, true) @@ -2097,36 +2574,36 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do kind: 12, name: "test \"does\" <> \"something\"", range: %{ - end: %{character: 10, line: 4}, - start: %{character: 10, line: 4} + "end" => %{"character" => 10, "line" => 4}, + "start" => %{"character" => 10, "line" => 4} }, selectionRange: %{ - end: %{character: 10, line: 4}, - start: %{character: 10, line: 4} + "end" => %{"character" => 10, "line" => 4}, + "start" => %{"character" => 10, "line" => 4} } } ], kind: 12, name: describe_sigil, range: %{ - end: %{character: 8, line: 3}, - start: %{character: 8, line: 3} + "end" => %{"character" => 8, "line" => 3}, + "start" => %{"character" => 8, "line" => 3} }, selectionRange: %{ - end: %{character: 8, line: 3}, - start: %{character: 8, line: 3} + "end" => %{"character" => 8, "line" => 3}, + "start" => %{"character" => 8, "line" => 3} } } ], kind: 2, name: "MyModuleTest", range: %{ - end: %{character: 6, line: 1}, - start: %{character: 6, line: 1} + "end" => %{"character" => 6, "line" => 1}, + "start" => %{"character" => 6, "line" => 1} }, selectionRange: %{ - end: %{character: 6, line: 1}, - start: %{character: 6, line: 1} + "end" => %{"character" => 6, "line" => 1}, + "start" => %{"character" => 6, "line" => 1} } } ]} = DocumentSymbols.symbols(uri, text, true) @@ -2155,14 +2632,20 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do name: "MyModuleTest", kind: 2, location: %{ - range: %{end: %{character: 6, line: 1}, start: %{character: 6, line: 1}} + range: %{ + "end" => %{"character" => 6, "line" => 1}, + "start" => %{"character" => 6, "line" => 1} + } } }, %Protocol.SymbolInformation{ name: "describe \"some description\"", kind: 12, location: %{ - range: %{end: %{character: 8, line: 3}, start: %{character: 8, line: 3}} + range: %{ + "end" => %{"character" => 8, "line" => 3}, + "start" => %{"character" => 8, "line" => 3} + } }, containerName: "MyModuleTest" }, @@ -2170,7 +2653,10 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do name: "test \"does something\"", kind: 12, location: %{ - range: %{end: %{character: 10, line: 4}, start: %{character: 10, line: 4}} + range: %{ + "end" => %{"character" => 10, "line" => 4}, + "start" => %{"character" => 10, "line" => 4} + } }, containerName: "describe \"some description\"" } @@ -2194,14 +2680,20 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do name: "MyModuleTest", kind: 2, location: %{ - range: %{end: %{character: 6, line: 1}, start: %{character: 6, line: 1}} + range: %{ + "end" => %{"character" => 6, "line" => 1}, + "start" => %{"character" => 6, "line" => 1} + } } }, %Protocol.SymbolInformation{ name: describe_sigil, kind: 12, location: %{ - range: %{end: %{character: 8, line: 3}, start: %{character: 8, line: 3}} + range: %{ + "end" => %{"character" => 8, "line" => 3}, + "start" => %{"character" => 8, "line" => 3} + } }, containerName: "MyModuleTest" }, @@ -2209,7 +2701,10 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do name: "test \"does\" <> \"something\"", kind: 12, location: %{ - range: %{end: %{character: 10, line: 4}, start: %{character: 10, line: 4}} + range: %{ + "end" => %{"character" => 10, "line" => 4}, + "start" => %{"character" => 10, "line" => 4} + } }, containerName: describe_sigil } @@ -2246,37 +2741,52 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do children: [], kind: 12, name: "setup", - range: %{end: %{character: 2, line: 2}, start: %{character: 2, line: 2}}, + range: %{ + "end" => %{"character" => 2, "line" => 2}, + "start" => %{"character" => 2, "line" => 2} + }, selectionRange: %{ - end: %{character: 2, line: 2}, - start: %{character: 2, line: 2} + "end" => %{"character" => 2, "line" => 2}, + "start" => %{"character" => 2, "line" => 2} } }, %Protocol.DocumentSymbol{ children: [], kind: 12, name: "setup", - range: %{end: %{character: 2, line: 5}, start: %{character: 2, line: 5}}, + range: %{ + "end" => %{"character" => 2, "line" => 5}, + "start" => %{"character" => 2, "line" => 5} + }, selectionRange: %{ - end: %{character: 2, line: 5}, - start: %{character: 2, line: 5} + "end" => %{"character" => 2, "line" => 5}, + "start" => %{"character" => 2, "line" => 5} } }, %Protocol.DocumentSymbol{ children: [], kind: 12, name: "setup_all", - range: %{end: %{character: 2, line: 6}, start: %{character: 2, line: 6}}, + range: %{ + "end" => %{"character" => 2, "line" => 6}, + "start" => %{"character" => 2, "line" => 6} + }, selectionRange: %{ - end: %{character: 2, line: 6}, - start: %{character: 2, line: 6} + "end" => %{"character" => 2, "line" => 6}, + "start" => %{"character" => 2, "line" => 6} } } ], kind: 2, name: "MyModuleTest", - range: %{end: %{character: 0, line: 0}, start: %{character: 0, line: 0}}, - selectionRange: %{end: %{character: 0, line: 0}, start: %{character: 0, line: 0}} + range: %{ + "end" => %{"character" => 0, "line" => 0}, + "start" => %{"character" => 0, "line" => 0} + }, + selectionRange: %{ + "end" => %{"character" => 0, "line" => 0}, + "start" => %{"character" => 0, "line" => 0} + } } ]} = DocumentSymbols.symbols(uri, text, true) end @@ -2303,14 +2813,20 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do name: "MyModuleTest", kind: 2, location: %{ - range: %{end: %{character: 0, line: 0}, start: %{character: 0, line: 0}} + range: %{ + "end" => %{"character" => 0, "line" => 0}, + "start" => %{"character" => 0, "line" => 0} + } } }, %Protocol.SymbolInformation{ name: "setup", kind: 12, location: %{ - range: %{end: %{character: 2, line: 2}, start: %{character: 2, line: 2}} + range: %{ + "end" => %{"character" => 2, "line" => 2}, + "start" => %{"character" => 2, "line" => 2} + } }, containerName: "MyModuleTest" }, @@ -2318,7 +2834,10 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do name: "setup", kind: 12, location: %{ - range: %{end: %{character: 2, line: 5}, start: %{character: 2, line: 5}} + range: %{ + "end" => %{"character" => 2, "line" => 5}, + "start" => %{"character" => 2, "line" => 5} + } }, containerName: "MyModuleTest" }, @@ -2326,7 +2845,10 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do name: "setup_all", kind: 12, location: %{ - range: %{end: %{character: 2, line: 6}, start: %{character: 2, line: 6}} + range: %{ + "end" => %{"character" => 2, "line" => 6}, + "start" => %{"character" => 2, "line" => 6} + } }, containerName: "MyModuleTest" } @@ -2356,29 +2878,53 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do children: [], kind: 20, name: "config :logger :console", - range: %{end: %{character: 0, line: 1}, start: %{character: 0, line: 1}}, - selectionRange: %{end: %{character: 0, line: 1}, start: %{character: 0, line: 1}} + range: %{ + "end" => %{"character" => 0, "line" => 1}, + "start" => %{"character" => 0, "line" => 1} + }, + selectionRange: %{ + "end" => %{"character" => 0, "line" => 1}, + "start" => %{"character" => 0, "line" => 1} + } }, %Protocol.DocumentSymbol{ children: [], kind: 20, name: "config :app :key", - range: %{end: %{character: 0, line: 6}, start: %{character: 0, line: 6}}, - selectionRange: %{end: %{character: 0, line: 6}, start: %{character: 0, line: 6}} + range: %{ + "end" => %{"character" => 0, "line" => 6}, + "start" => %{"character" => 0, "line" => 6} + }, + selectionRange: %{ + "end" => %{"character" => 0, "line" => 6}, + "start" => %{"character" => 0, "line" => 6} + } }, %Protocol.DocumentSymbol{ children: [], kind: 20, name: "config :my_app :ecto_repos", - range: %{end: %{character: 0, line: 7}, start: %{character: 0, line: 7}}, - selectionRange: %{end: %{character: 0, line: 7}, start: %{character: 0, line: 7}} + range: %{ + "end" => %{"character" => 0, "line" => 7}, + "start" => %{"character" => 0, "line" => 7} + }, + selectionRange: %{ + "end" => %{"character" => 0, "line" => 7}, + "start" => %{"character" => 0, "line" => 7} + } }, %Protocol.DocumentSymbol{ children: [], kind: 20, name: "config :my_app MyApp.Repo", - range: %{end: %{character: 0, line: 9}, start: %{character: 0, line: 9}}, - selectionRange: %{end: %{character: 0, line: 9}, start: %{character: 0, line: 9}} + range: %{ + "end" => %{"character" => 0, "line" => 9}, + "start" => %{"character" => 0, "line" => 9} + }, + selectionRange: %{ + "end" => %{"character" => 0, "line" => 9}, + "start" => %{"character" => 0, "line" => 9} + } } ]} = DocumentSymbols.symbols(uri, text, true) end @@ -2406,28 +2952,40 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do name: "config :logger :console", kind: 20, location: %{ - range: %{end: %{character: 0, line: 1}, start: %{character: 0, line: 1}} + range: %{ + "end" => %{"character" => 0, "line" => 1}, + "start" => %{"character" => 0, "line" => 1} + } } }, %Protocol.SymbolInformation{ name: "config :app :key", kind: 20, location: %{ - range: %{end: %{character: 0, line: 6}, start: %{character: 0, line: 6}} + range: %{ + "end" => %{"character" => 0, "line" => 6}, + "start" => %{"character" => 0, "line" => 6} + } } }, %Protocol.SymbolInformation{ name: "config :my_app :ecto_repos", kind: 20, location: %{ - range: %{end: %{character: 0, line: 7}, start: %{character: 0, line: 7}} + range: %{ + "end" => %{"character" => 0, "line" => 7}, + "start" => %{"character" => 0, "line" => 7} + } } }, %Protocol.SymbolInformation{ name: "config :my_app MyApp.Repo", kind: 20, location: %{ - range: %{end: %{character: 0, line: 9}, start: %{character: 0, line: 9}} + range: %{ + "end" => %{"character" => 0, "line" => 9}, + "start" => %{"character" => 0, "line" => 9} + } } } ]} = DocumentSymbols.symbols(uri, text, false) @@ -2450,12 +3008,12 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do kind: 2, name: "MISSING_MODULE_NAME", range: %{ - start: %{line: 0, character: 0}, - end: %{line: 0, character: 0} + "start" => %{"line" => 0, "character" => 0}, + "end" => %{"line" => 0, "character" => 0} }, selectionRange: %{ - start: %{line: 0, character: 0}, - end: %{line: 0, character: 0} + "start" => %{"line" => 0, "character" => 0}, + "end" => %{"line" => 0, "character" => 0} } } ] = document_symbols @@ -2466,12 +3024,12 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do kind: 12, name: "def foo", range: %{ - start: %{character: 4, line: 1}, - end: %{character: 4, line: 1} + "start" => %{"character" => 4, "line" => 1}, + "end" => %{"character" => 4, "line" => 1} }, selectionRange: %{ - start: %{character: 4, line: 1}, - end: %{character: 4, line: 1} + "start" => %{"character" => 4, "line" => 1}, + "end" => %{"character" => 4, "line" => 1} } } ] @@ -2493,12 +3051,12 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do kind: 11, name: "MISSING_PROTOCOL_NAME", range: %{ - start: %{line: 0, character: 0}, - end: %{line: 0, character: 0} + "start" => %{"line" => 0, "character" => 0}, + "end" => %{"line" => 0, "character" => 0} }, selectionRange: %{ - start: %{line: 0, character: 0}, - end: %{line: 0, character: 0} + "start" => %{"line" => 0, "character" => 0}, + "end" => %{"line" => 0, "character" => 0} } } ] diff --git a/apps/language_server/test/source_file_test.exs b/apps/language_server/test/source_file_test.exs index f5b79a56a..6a79087d8 100644 --- a/apps/language_server/test/source_file_test.exs +++ b/apps/language_server/test/source_file_test.exs @@ -822,4 +822,57 @@ defmodule ElixirLS.LanguageServer.SourceFileTest do end end end + + describe "positions" do + test "lsp_position_to_elixr empty" do + assert {1, 1} == SourceFile.lsp_position_to_elixr("", {0, 0}) + end + + test "lsp_position_to_elixr single first char" do + assert {1, 1} == SourceFile.lsp_position_to_elixr("abcde", {0, 0}) + end + + test "lsp_position_to_elixr single line" do + assert {1, 2} == SourceFile.lsp_position_to_elixr("abcde", {0, 1}) + end + + test "lsp_position_to_elixr single line utf8" do + assert {1, 2} == SourceFile.lsp_position_to_elixr("🏳️‍🌈abcde", {0, 6}) + end + + test "lsp_position_to_elixr multi line" do + assert {2, 2} == SourceFile.lsp_position_to_elixr("abcde\n1234", {1, 1}) + end + + test "elixir_position_to_lsp empty" do + assert {0, 0} == SourceFile.elixir_position_to_lsp("", {1, 1}) + end + + test "elixir_position_to_lsp single line first char" do + assert {0, 0} == SourceFile.elixir_position_to_lsp("abcde", {1, 1}) + end + + test "elixir_position_to_lsp single line" do + assert {0, 1} == SourceFile.elixir_position_to_lsp("abcde", {1, 2}) + end + + test "elixir_position_to_lsp single line utf8" do + assert {0, 6} == SourceFile.elixir_position_to_lsp("🏳️‍🌈abcde", {1, 2}) + end + + test "elixir_position_to_lsp multi line" do + assert {1, 1} == SourceFile.elixir_position_to_lsp("abcde\n1234", {2, 2}) + end + + test "sanity check" do + text = "aąłsd🏳️‍🌈abcde" + + for i <- 0..String.length(text) do + elixir_pos = {1, i + 1} + lsp_pos = SourceFile.elixir_position_to_lsp(text, elixir_pos) + + assert elixir_pos == SourceFile.lsp_position_to_elixr(text, lsp_pos) + end + end + end end From ea250e270399382e73ea4754bf6d5976e6f95897 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Samson?= Date: Sat, 19 Feb 2022 12:56:35 +0100 Subject: [PATCH 005/125] Make debugger conform to DAP 1.51 continue/stepin/stepout/stepover (#678) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * remove not supported SetExceptionBreakpoints request handler Clients should only call this request if the capability ‘exceptionBreakpointFilters’ returns one or more filters. No way to implement it via :int module * implement function breakpoints * test erlang breakpoints * fix small memory leak when unsetting last breakpoint in file * add more breakpoint tests * make tests more synchronous * add function breakpoints tests * interpret modules * extract and test mfa parsing * run formatter * Update readme * cleanup * extract and test erlang binding processing fix some isses when var is usend more than 10 times add explicite ordering by variable instance discard underscored variables * breakpoint conditions support added do not warn when setting already set breakpoint * update readme * log when expression crashes * add support for conditional function breakpoints * update readme * add support for breakpoint hit condition * readme updated * implement log message * readme updated * add support for terminateThreads request * add support for pause * make continue, next, stepIn, stepout requests conform to DAP 1.51 Fixes https://github.com/elixir-lsp/elixir-ls/issues/669 reworks fixes for https://github.com/elixir-lsp/elixir-ls/issues/455 * cleanup :int workaround * tests wip * format --- .../elixir_ls_debugger/lib/debugger/server.ex | 120 +++++++++--------- .../elixir_ls_debugger/test/debugger_test.exs | 4 +- 2 files changed, 64 insertions(+), 60 deletions(-) diff --git a/apps/elixir_ls_debugger/lib/debugger/server.ex b/apps/elixir_ls_debugger/lib/debugger/server.ex index b5e9d99b7..acae675f6 100644 --- a/apps/elixir_ls_debugger/lib/debugger/server.ex +++ b/apps/elixir_ls_debugger/lib/debugger/server.ex @@ -186,11 +186,12 @@ defmodule ElixirLS.Debugger.Server do ) {thread_id, threads_inverse} = state.threads_inverse |> Map.pop(pid) - state = remove_paused_process(state, pid) + paused_processes = remove_paused_process(state, pid) state = %{ state | threads: state.threads |> Map.delete(thread_id), + paused_processes: paused_processes, threads_inverse: threads_inverse } @@ -565,76 +566,48 @@ defmodule ElixirLS.Debugger.Server do {json, state} end - defp handle_request(continue_req(_, thread_id), state = %__MODULE__{}) do + defp handle_request(continue_req(_, thread_id) = args, state = %__MODULE__{}) do pid = get_pid_by_thread_id!(state, thread_id) - try do - :int.continue(pid) - state = remove_paused_process(state, pid) - {%{"allThreadsContinued" => false}, state} - rescue - e in MatchError -> - raise ServerError, - message: "serverError", - format: ":int.continue failed: {message}", - variables: %{ - "message" => inspect(Exception.message(e)) - } - end + safe_int_action(pid, :continue) + + paused_processes = remove_paused_process(state, pid) + paused_processes = maybe_continue_other_processes(args, paused_processes, pid) + + processes_paused? = paused_processes |> Map.keys() |> Enum.any?(&is_pid/1) + + {%{"allThreadsContinued" => not processes_paused?}, + %{state | paused_processes: paused_processes}} end - defp handle_request(next_req(_, thread_id), state = %__MODULE__{}) do + defp handle_request(next_req(_, thread_id) = args, state = %__MODULE__{}) do pid = get_pid_by_thread_id!(state, thread_id) - try do - :int.next(pid) - state = remove_paused_process(state, pid) - {%{}, state} - rescue - e in MatchError -> - raise ServerError, - message: "serverError", - format: ":int.next failed: {message}", - variables: %{ - "message" => inspect(Exception.message(e)) - } - end + safe_int_action(pid, :next) + paused_processes = remove_paused_process(state, pid) + + {%{}, + %{state | paused_processes: maybe_continue_other_processes(args, paused_processes, pid)}} end - defp handle_request(step_in_req(_, thread_id), state = %__MODULE__{}) do + defp handle_request(step_in_req(_, thread_id) = args, state = %__MODULE__{}) do pid = get_pid_by_thread_id!(state, thread_id) - try do - :int.step(pid) - state = remove_paused_process(state, pid) - {%{}, state} - rescue - e in MatchError -> - raise ServerError, - message: "serverError", - format: ":int.stop failed: {message}", - variables: %{ - "message" => inspect(Exception.message(e)) - } - end + safe_int_action(pid, :step) + paused_processes = remove_paused_process(state, pid) + + {%{}, + %{state | paused_processes: maybe_continue_other_processes(args, paused_processes, pid)}} end - defp handle_request(step_out_req(_, thread_id), state = %__MODULE__{}) do + defp handle_request(step_out_req(_, thread_id) = args, state = %__MODULE__{}) do pid = get_pid_by_thread_id!(state, thread_id) - try do - :int.finish(pid) - state = remove_paused_process(state, pid) - {%{}, state} - rescue - e in MatchError -> - raise ServerError, - message: "serverError", - format: ":int.finish failed: {message}", - variables: %{ - "message" => inspect(Exception.message(e)) - } - end + safe_int_action(pid, :finish) + paused_processes = remove_paused_process(state, pid) + + {%{}, + %{state | paused_processes: maybe_continue_other_processes(args, paused_processes, pid)}} end defp handle_request(request(_, command), _state = %__MODULE__{}) when is_binary(command) do @@ -646,6 +619,36 @@ defmodule ElixirLS.Debugger.Server do } end + defp maybe_continue_other_processes(%{"singleThread" => true}, paused_processes, requested_pid) do + resumed_pids = + for {paused_pid, %PausedProcess{ref: ref}} when paused_pid != requested_pid <- + paused_processes do + safe_int_action(paused_pid, :continue) + true = Process.demonitor(ref, [:flush]) + paused_pid + end + + paused_processes |> Map.drop(resumed_pids) + end + + defp maybe_continue_other_processes(_, paused_processes, _requested_pid), do: paused_processes + + # TODO consider removing this workaround as the problem seems to no longer affect OTP 24 + defp safe_int_action(pid, action) do + apply(:int, action, [pid]) + :ok + catch + kind, payload -> + # when stepping out of interpreted code a MatchError is risen inside :int module (at least in OTP 23) + IO.warn(":int.#{action}(#{inspect(pid)}) failed: #{Exception.format(kind, payload)}") + + unless action == :continue do + safe_int_action(pid, :continue) + end + + :ok + end + defp get_pid_by_thread_id!(state = %__MODULE__{}, thread_id) do case state.threads[thread_id] do nil -> @@ -668,7 +671,7 @@ defmodule ElixirLS.Debugger.Server do true = Process.demonitor(process.ref, [:flush]) end - %__MODULE__{state | paused_processes: paused_processes} + paused_processes end defp variables(state = %__MODULE__{}, pid, var, start, count, filter) do @@ -948,6 +951,7 @@ defmodule ElixirLS.Debugger.Server do "supportsValueFormattingOptions" => false, "supportsExceptionInfoRequest" => false, "supportsTerminateThreadsRequest" => true, + "supportsSingleThreadExecutionRequests" => true, "supportTerminateDebuggee" => false } end diff --git a/apps/elixir_ls_debugger/test/debugger_test.exs b/apps/elixir_ls_debugger/test/debugger_test.exs index da6601479..4075d2ada 100644 --- a/apps/elixir_ls_debugger/test/debugger_test.exs +++ b/apps/elixir_ls_debugger/test/debugger_test.exs @@ -196,7 +196,7 @@ defmodule ElixirLS.Debugger.ServerTest do 1000 Server.receive_packet(server, continue_req(10, thread_id)) - assert_receive response(_, 10, "continue", %{"allThreadsContinued" => false}) + assert_receive response(_, 10, "continue", %{"allThreadsContinued" => true}) end) end @@ -333,7 +333,7 @@ defmodule ElixirLS.Debugger.ServerTest do ) Server.receive_packet(server, continue_req(15, thread_id)) - assert_receive response(_, 15, "continue", %{"allThreadsContinued" => false}) + assert_receive response(_, 15, "continue", %{"allThreadsContinued" => true}) Server.receive_packet(server, stacktrace_req(7, thread_id)) thread_id_str = inspect(thread_id) From a0e60f194476cd2044c0a8546bfa86ce3148b174 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Samson?= Date: Mon, 21 Feb 2022 08:11:41 +0100 Subject: [PATCH 006/125] Add support for hover and clipboard eval in debugger (#680) * add support for hover and clipboard eval in debugger * use empty string --- .../elixir_ls_debugger/lib/debugger/server.ex | 45 ++++++++++++++----- 1 file changed, 33 insertions(+), 12 deletions(-) diff --git a/apps/elixir_ls_debugger/lib/debugger/server.ex b/apps/elixir_ls_debugger/lib/debugger/server.ex index acae675f6..582b0d7dc 100644 --- a/apps/elixir_ls_debugger/lib/debugger/server.ex +++ b/apps/elixir_ls_debugger/lib/debugger/server.ex @@ -550,20 +550,39 @@ defmodule ElixirLS.Debugger.Server do timeout = Map.get(state.config, "debugExpressionTimeoutMs", 10_000) bindings = all_variables(state.paused_processes, args["frameId"]) - value = evaluate_code_expression(expr, bindings, timeout) + result = evaluate_code_expression(expr, bindings, timeout) - child_type = Variables.child_type(value) - {state, var_id} = get_variable_reference(child_type, state, :evaluator, value) + case result do + {:ok, value} -> + child_type = Variables.child_type(value) + {state, var_id} = get_variable_reference(child_type, state, :evaluator, value) + + 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 = - %{ - "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} - {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 + } + + {json, state} + end end defp handle_request(continue_req(_, thread_id) = args, state = %__MODULE__{}) do @@ -739,7 +758,7 @@ defmodule ElixirLS.Debugger.Server do result = Task.yield(task, timeout) || Task.shutdown(task) case result do - {:ok, data} -> data + {:ok, data} -> {:ok, data} nil -> :elixir_ls_expression_timeout _otherwise -> result end @@ -952,6 +971,8 @@ defmodule ElixirLS.Debugger.Server do "supportsExceptionInfoRequest" => false, "supportsTerminateThreadsRequest" => true, "supportsSingleThreadExecutionRequests" => true, + "supportsEvaluateForHovers" => true, + "supportsClipboardContext" => true, "supportTerminateDebuggee" => false } end From ba6252f4edb6909a6ce877834178ac7d0ff3f21d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Samson?= Date: Mon, 21 Feb 2022 17:49:48 +0100 Subject: [PATCH 007/125] Add support for completions in debugger (#679) * fix typo * warn about not supported client options * Add support for debugger completions --- .../lib/debugger/completions.ex | 56 ++++++++++++++++++ .../lib/debugger/protocol.ex | 6 ++ .../elixir_ls_debugger/lib/debugger/server.ex | 57 ++++++++++++++++++- apps/elixir_ls_debugger/lib/debugger/utils.ex | 31 ++++++++++ .../elixir_ls_debugger/test/debugger_test.exs | 24 ++++++++ apps/elixir_ls_debugger/test/utils_test.exs | 18 ++++++ .../language_server/providers/definition.ex | 2 +- .../lib/language_server/providers/hover.ex | 2 +- .../providers/implementation.ex | 2 +- .../language_server/providers/references.ex | 2 +- .../providers/signature_help.ex | 2 +- .../lib/language_server/source_file.ex | 4 +- .../language_server/test/source_file_test.exs | 22 +++---- mix.lock | 2 +- 14 files changed, 209 insertions(+), 21 deletions(-) create mode 100644 apps/elixir_ls_debugger/lib/debugger/completions.ex diff --git a/apps/elixir_ls_debugger/lib/debugger/completions.ex b/apps/elixir_ls_debugger/lib/debugger/completions.ex new file mode 100644 index 000000000..17be5216a --- /dev/null +++ b/apps/elixir_ls_debugger/lib/debugger/completions.ex @@ -0,0 +1,56 @@ +defmodule ElixirLS.Debugger.Completions do + # type CompletionItemType = 'method' | 'function' | 'constructor' | 'field' + # | 'variable' | 'class' | 'interface' | 'module' | 'property' | 'unit' + # | 'value' | 'enum' | 'keyword' | 'snippet' | 'text' | 'color' | 'file' + # | 'reference' | 'customcolor'; + def map(%{ + type: type, + name: name, + arity: arity, + snippet: snippet + }) + when type in [:function, :macro] do + %{ + type: "function", + label: "#{name}/#{arity}", + text: snippet || name + } + end + + def map(%{ + type: :module, + name: name + }) do + text = + case name do + ":" <> rest -> rest + other -> other + end + + %{ + type: "module", + label: name, + text: text + } + end + + def map(%{ + type: :variable, + name: name + }) do + %{ + type: "variable", + label: name + } + end + + def map(%{ + type: :field, + name: name + }) do + %{ + type: "field", + label: name + } + end +end diff --git a/apps/elixir_ls_debugger/lib/debugger/protocol.ex b/apps/elixir_ls_debugger/lib/debugger/protocol.ex index b7b231915..fb23e916d 100644 --- a/apps/elixir_ls_debugger/lib/debugger/protocol.ex +++ b/apps/elixir_ls_debugger/lib/debugger/protocol.ex @@ -85,6 +85,12 @@ defmodule ElixirLS.Debugger.Protocol do end end + defmacro completions_req(seq, text) do + quote do + request(unquote(seq), "completions", %{"text" => unquote(text)}) + end + end + defmacro continue_req(seq, thread_id) do quote do request(unquote(seq), "continue", %{"threadId" => unquote(thread_id)}) diff --git a/apps/elixir_ls_debugger/lib/debugger/server.ex b/apps/elixir_ls_debugger/lib/debugger/server.ex index 582b0d7dc..827f35cc9 100644 --- a/apps/elixir_ls_debugger/lib/debugger/server.ex +++ b/apps/elixir_ls_debugger/lib/debugger/server.ex @@ -228,6 +228,21 @@ defmodule ElixirLS.Debugger.Server do ## Helpers defp handle_request(initialize_req(_, client_info), %__MODULE__{client_info: nil} = state) do + # linesStartAt1 is true by default and we only support 1-based indexing + if client_info["linesStartAt1"] == false do + IO.warn("0-based lines are not supported") + end + + # columnsStartAt1 is true by default and we only support 1-based indexing + if client_info["columnsStartAt1"] == false do + IO.warn("0-based columns are not supported") + end + + # pathFormat is `path` by default and we do not support other, e.g. `uri` + if client_info["pathFormat"] not in [nil, "path"] do + IO.warn("pathFormat #{client_info["pathFormat"]} not supported") + end + {capabilities(), %{state | client_info: client_info}} end @@ -240,7 +255,11 @@ defmodule ElixirLS.Debugger.Server do } end - defp handle_request(launch_req(_, config), state = %__MODULE__{}) do + defp handle_request(launch_req(_, config) = args, state = %__MODULE__{}) do + if args["arguments"]["noDebug"] == true do + IO.warn("launch with no debug is not supported") + end + {_, ref} = spawn_monitor(fn -> initialize(config) end) receive do @@ -629,6 +648,39 @@ defmodule ElixirLS.Debugger.Server do %{state | paused_processes: maybe_continue_other_processes(args, paused_processes, pid)}} 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 + + # for simplicity take only text from the given line up to column + line = + text + |> String.split(["\r\n", "\n", "\r"]) + |> Enum.at(line) + + # it's not documented but VSCode uses utf16 positions + column = Utils.dap_character_to_elixir(line, column) + prefix = String.slice(line, 0, column) + + vars = + all_variables(state.paused_processes, args["arguments"]["frameId"]) + |> Enum.map(fn {name, value} -> + %ElixirSense.Core.State.VarInfo{ + name: name, + type: ElixirSense.Core.Binding.from_var(value) + } + end) + + env = %ElixirSense.Providers.Suggestion.Complete.Env{vars: vars} + + results = + ElixirSense.Providers.Suggestion.Complete.complete(prefix, env) + |> Enum.map(&ElixirLS.Debugger.Completions.map/1) + + {%{"targets" => results}, state} + end + defp handle_request(request(_, command), _state = %__MODULE__{}) when is_binary(command) do raise ServerError, message: "notSupported", @@ -961,7 +1013,8 @@ defmodule ElixirLS.Debugger.Server do "supportsRestartFrame" => false, "supportsGotoTargetsRequest" => false, "supportsStepInTargetsRequest" => false, - "supportsCompletionsRequest" => false, + "supportsCompletionsRequest" => true, + "completionTriggerCharacters" => [".", "&", "%", "^", ":", "!", "-", "~"], "supportsModulesRequest" => false, "additionalModuleColumns" => [], "supportedChecksumAlgorithms" => [], diff --git a/apps/elixir_ls_debugger/lib/debugger/utils.ex b/apps/elixir_ls_debugger/lib/debugger/utils.ex index 742e07d73..9ab0eb706 100644 --- a/apps/elixir_ls_debugger/lib/debugger/utils.ex +++ b/apps/elixir_ls_debugger/lib/debugger/utils.ex @@ -18,4 +18,35 @@ defmodule ElixirLS.Debugger.Utils do {:error, "cannot parse MFA"} end end + + defp characters_to_binary!(binary, from, to) do + case :unicode.characters_to_binary(binary, from, to) do + result when is_binary(result) -> result + end + end + + def dap_character_to_elixir(_utf8_line, dap_character) when dap_character <= 0, do: 0 + + def dap_character_to_elixir(utf8_line, dap_character) do + utf16_line = + utf8_line + |> characters_to_binary!(:utf8, :utf16) + + byte_size = byte_size(utf16_line) + + # if character index is over the length of the string assume we pad it with spaces (1 byte in utf8) + diff = div(max(dap_character * 2 - byte_size, 0), 2) + + utf8_character = + utf16_line + |> (&binary_part( + &1, + 0, + min(dap_character * 2, byte_size) + )).() + |> characters_to_binary!(:utf16, :utf8) + |> String.length() + + utf8_character + diff + end end diff --git a/apps/elixir_ls_debugger/test/debugger_test.exs b/apps/elixir_ls_debugger/test/debugger_test.exs index 4075d2ada..e6fd317e5 100644 --- a/apps/elixir_ls_debugger/test/debugger_test.exs +++ b/apps/elixir_ls_debugger/test/debugger_test.exs @@ -1545,4 +1545,28 @@ defmodule ElixirLS.Debugger.ServerTest do assert Process.alive?(server) end end + + test "Completions", %{server: server} do + 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" + } + ) + + assert_receive(%{"body" => %{"targets" => _targets}}, 10000) + + assert Process.alive?(server) + + # assert [%{}] + end end diff --git a/apps/elixir_ls_debugger/test/utils_test.exs b/apps/elixir_ls_debugger/test/utils_test.exs index 4f91eb69c..d2e5b60f0 100644 --- a/apps/elixir_ls_debugger/test/utils_test.exs +++ b/apps/elixir_ls_debugger/test/utils_test.exs @@ -33,4 +33,22 @@ defmodule ElixirLS.Debugger.UtilsTest do assert {:error, "cannot parse MFA"} == Utils.parse_mfa("") end end + + describe "positions" do + test "dap_character_to_elixir empty" do + assert 0 == Utils.dap_character_to_elixir("", 0) + end + + test "dap_character_to_elixir first char" do + assert 0 == Utils.dap_character_to_elixir("abcde", 0) + end + + test "dap_character_to_elixir line" do + assert 1 == Utils.dap_character_to_elixir("abcde", 1) + end + + test "dap_character_to_elixir utf8" do + assert 1 == Utils.dap_character_to_elixir("🏳️‍🌈abcde", 6) + end + end end diff --git a/apps/language_server/lib/language_server/providers/definition.ex b/apps/language_server/lib/language_server/providers/definition.ex index 65dd62128..1898a5c73 100644 --- a/apps/language_server/lib/language_server/providers/definition.ex +++ b/apps/language_server/lib/language_server/providers/definition.ex @@ -6,7 +6,7 @@ defmodule ElixirLS.LanguageServer.Providers.Definition do alias ElixirLS.LanguageServer.{Protocol, SourceFile} def definition(uri, text, line, character) do - {line, character} = SourceFile.lsp_position_to_elixr(text, {line, character}) + {line, character} = SourceFile.lsp_position_to_elixir(text, {line, character}) result = case ElixirSense.definition(text, line, character) do diff --git a/apps/language_server/lib/language_server/providers/hover.ex b/apps/language_server/lib/language_server/providers/hover.ex index c2e470762..cf2bd75ec 100644 --- a/apps/language_server/lib/language_server/providers/hover.ex +++ b/apps/language_server/lib/language_server/providers/hover.ex @@ -18,7 +18,7 @@ defmodule ElixirLS.LanguageServer.Providers.Hover do |> Enum.map(fn x -> "lib/#{x}/lib" end) def hover(text, line, character, project_dir) do - {line, character} = SourceFile.lsp_position_to_elixr(text, {line, character}) + {line, character} = SourceFile.lsp_position_to_elixir(text, {line, character}) response = case ElixirSense.docs(text, line, character) do diff --git a/apps/language_server/lib/language_server/providers/implementation.ex b/apps/language_server/lib/language_server/providers/implementation.ex index 920139901..0cf874375 100644 --- a/apps/language_server/lib/language_server/providers/implementation.ex +++ b/apps/language_server/lib/language_server/providers/implementation.ex @@ -6,7 +6,7 @@ defmodule ElixirLS.LanguageServer.Providers.Implementation do alias ElixirLS.LanguageServer.{Protocol, SourceFile} def implementation(uri, text, line, character) do - {line, character} = SourceFile.lsp_position_to_elixr(text, {line, character}) + {line, character} = SourceFile.lsp_position_to_elixir(text, {line, character}) locations = ElixirSense.implementations(text, line, character) results = for location <- locations, do: Protocol.Location.new(location, uri, text) diff --git a/apps/language_server/lib/language_server/providers/references.ex b/apps/language_server/lib/language_server/providers/references.ex index b2612ea9e..7188ac608 100644 --- a/apps/language_server/lib/language_server/providers/references.ex +++ b/apps/language_server/lib/language_server/providers/references.ex @@ -14,7 +14,7 @@ defmodule ElixirLS.LanguageServer.Providers.References do import ElixirLS.LanguageServer.Protocol def references(text, uri, line, character, _include_declaration) do - {line, character} = SourceFile.lsp_position_to_elixr(text, {line, character}) + {line, character} = SourceFile.lsp_position_to_elixir(text, {line, character}) Build.with_build_lock(fn -> ElixirSense.references(text, line, character) diff --git a/apps/language_server/lib/language_server/providers/signature_help.ex b/apps/language_server/lib/language_server/providers/signature_help.ex index 9b8ae94e9..7f9a615e5 100644 --- a/apps/language_server/lib/language_server/providers/signature_help.ex +++ b/apps/language_server/lib/language_server/providers/signature_help.ex @@ -4,7 +4,7 @@ defmodule ElixirLS.LanguageServer.Providers.SignatureHelp do def trigger_characters(), do: ["(", ","] def signature(%SourceFile{} = source_file, line, character) do - {line, character} = SourceFile.lsp_position_to_elixr(source_file.text, {line, character}) + {line, character} = SourceFile.lsp_position_to_elixir(source_file.text, {line, character}) response = case ElixirSense.signature(source_file.text, line, character) do diff --git a/apps/language_server/lib/language_server/source_file.ex b/apps/language_server/lib/language_server/source_file.ex index 5f02ea1c4..57cbe02a7 100644 --- a/apps/language_server/lib/language_server/source_file.ex +++ b/apps/language_server/lib/language_server/source_file.ex @@ -396,10 +396,10 @@ defmodule ElixirLS.LanguageServer.SourceFile do utf8_character + 1 + diff end - def lsp_position_to_elixr(_urf8_text, {lsp_line, lsp_character}) when lsp_character <= 0, + def lsp_position_to_elixir(_urf8_text, {lsp_line, lsp_character}) when lsp_character <= 0, do: {max(lsp_line + 1, 1), 1} - def lsp_position_to_elixr(urf8_text, {lsp_line, lsp_character}) do + def lsp_position_to_elixir(urf8_text, {lsp_line, lsp_character}) do utf8_character = lines(urf8_text) |> Enum.at(max(lsp_line, 0)) diff --git a/apps/language_server/test/source_file_test.exs b/apps/language_server/test/source_file_test.exs index 6a79087d8..86e69d00f 100644 --- a/apps/language_server/test/source_file_test.exs +++ b/apps/language_server/test/source_file_test.exs @@ -824,24 +824,24 @@ defmodule ElixirLS.LanguageServer.SourceFileTest do end describe "positions" do - test "lsp_position_to_elixr empty" do - assert {1, 1} == SourceFile.lsp_position_to_elixr("", {0, 0}) + test "lsp_position_to_elixir empty" do + assert {1, 1} == SourceFile.lsp_position_to_elixir("", {0, 0}) end - test "lsp_position_to_elixr single first char" do - assert {1, 1} == SourceFile.lsp_position_to_elixr("abcde", {0, 0}) + test "lsp_position_to_elixir single first char" do + assert {1, 1} == SourceFile.lsp_position_to_elixir("abcde", {0, 0}) end - test "lsp_position_to_elixr single line" do - assert {1, 2} == SourceFile.lsp_position_to_elixr("abcde", {0, 1}) + test "lsp_position_to_elixir single line" do + assert {1, 2} == SourceFile.lsp_position_to_elixir("abcde", {0, 1}) end - test "lsp_position_to_elixr single line utf8" do - assert {1, 2} == SourceFile.lsp_position_to_elixr("🏳️‍🌈abcde", {0, 6}) + test "lsp_position_to_elixir single line utf8" do + assert {1, 2} == SourceFile.lsp_position_to_elixir("🏳️‍🌈abcde", {0, 6}) end - test "lsp_position_to_elixr multi line" do - assert {2, 2} == SourceFile.lsp_position_to_elixr("abcde\n1234", {1, 1}) + test "lsp_position_to_elixir multi line" do + assert {2, 2} == SourceFile.lsp_position_to_elixir("abcde\n1234", {1, 1}) end test "elixir_position_to_lsp empty" do @@ -871,7 +871,7 @@ defmodule ElixirLS.LanguageServer.SourceFileTest do elixir_pos = {1, i + 1} lsp_pos = SourceFile.elixir_position_to_lsp(text, elixir_pos) - assert elixir_pos == SourceFile.lsp_position_to_elixr(text, lsp_pos) + assert elixir_pos == SourceFile.lsp_position_to_elixir(text, lsp_pos) end end end diff --git a/mix.lock b/mix.lock index 70ecd4a42..82bde6e0a 100644 --- a/mix.lock +++ b/mix.lock @@ -1,7 +1,7 @@ %{ "dialyxir": {:hex, :dialyxir, "1.1.0", "c5aab0d6e71e5522e77beff7ba9e08f8e02bad90dfbeffae60eaf0cb47e29488", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "07ea8e49c45f15264ebe6d5b93799d4dd56a44036cf42d0ad9c960bc266c0b9a"}, "docsh": {:hex, :docsh, "0.7.2", "f893d5317a0e14269dd7fe79cf95fb6b9ba23513da0480ec6e77c73221cae4f2", [:rebar3], [{:providers, "1.8.1", [hex: :providers, repo: "hexpm", optional: false]}], "hexpm", "4e7db461bb07540d2bc3d366b8513f0197712d0495bb85744f367d3815076134"}, - "elixir_sense": {:git, "https://github.com/elixir-lsp/elixir_sense.git", "33df514a1254455f54cb069999454c7e8586eb2d", []}, + "elixir_sense": {:git, "https://github.com/elixir-lsp/elixir_sense.git", "49e9d4ffffa6e97ee5acca29dded100e49a73ebb", []}, "erl2ex": {:git, "https://github.com/dazuma/erl2ex.git", "244c2d9ed5805ef4855a491d8616b8842fef7ca4", []}, "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, "getopt": {:hex, :getopt, "1.0.1", "c73a9fa687b217f2ff79f68a3b637711bb1936e712b521d8ce466b29cbf7808a", [:rebar3], [], "hexpm", "53e1ab83b9ceb65c9672d3e7a35b8092e9bdc9b3ee80721471a161c10c59959c"}, From 99ab6e98ff181aae01ca3d119dee0ea9c49c727a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Samson?= Date: Sun, 6 Mar 2022 20:45:13 +0100 Subject: [PATCH 008/125] Add debugger variable scopes with messages and process info (#681) * delete duplicated capability * add process messages as debugger variable scope * improve display of keyword variables * add process info as debugger variable scope --- .../elixir_ls_debugger/lib/debugger/server.ex | 26 +++++++++++++-- .../lib/debugger/stacktrace.ex | 10 ++++-- .../lib/debugger/variables.ex | 32 ++++++++++++++++--- .../elixir_ls_debugger/test/debugger_test.exs | 7 ++++ .../test/variables_test.exs | 18 +++++++++++ 5 files changed, 83 insertions(+), 10 deletions(-) diff --git a/apps/elixir_ls_debugger/lib/debugger/server.ex b/apps/elixir_ls_debugger/lib/debugger/server.ex index 827f35cc9..edc78008c 100644 --- a/apps/elixir_ls_debugger/lib/debugger/server.ex +++ b/apps/elixir_ls_debugger/lib/debugger/server.ex @@ -509,6 +509,9 @@ defmodule ElixirLS.Debugger.Server do {pid, %Frame{} = frame} -> {state, args_id} = ensure_var_id(state, pid, frame.args) {state, bindings_id} = ensure_var_id(state, pid, frame.bindings) + {state, messages_id} = ensure_var_id(state, pid, frame.messages) + process_info = Process.info(pid) + {state, process_info_id} = ensure_var_id(state, pid, process_info) vars_scope = %{ "name" => "variables", @@ -526,7 +529,27 @@ defmodule ElixirLS.Debugger.Server do "expensive" => false } - scopes = if Enum.count(frame.args) > 0, do: [vars_scope, args_scope], else: [vars_scope] + messages_scope = %{ + "name" => "messages", + "variablesReference" => messages_id, + "namedVariables" => 0, + "indexedVariables" => Enum.count(frame.messages), + "expensive" => false + } + + process_info_scope = %{ + "name" => "process info", + "variablesReference" => process_info_id, + "namedVariables" => length(process_info), + "indexedVariables" => 0, + "expensive" => false + } + + scopes = + [vars_scope, process_info_scope] + |> Kernel.++(if Enum.count(frame.args) > 0, do: [args_scope], else: []) + |> Kernel.++(if Enum.count(frame.messages) > 0, do: [messages_scope], else: []) + {state, scopes} nil -> @@ -1006,7 +1029,6 @@ defmodule ElixirLS.Debugger.Server do "supportsConditionalBreakpoints" => true, "supportsHitConditionalBreakpoints" => true, "supportsLogPoints" => true, - "supportsEvaluateForHovers" => false, "exceptionBreakpointFilters" => [], "supportsStepBack" => false, "supportsSetVariable" => false, diff --git a/apps/elixir_ls_debugger/lib/debugger/stacktrace.ex b/apps/elixir_ls_debugger/lib/debugger/stacktrace.ex index 677301614..efbf20299 100644 --- a/apps/elixir_ls_debugger/lib/debugger/stacktrace.ex +++ b/apps/elixir_ls_debugger/lib/debugger/stacktrace.ex @@ -4,7 +4,7 @@ defmodule ElixirLS.Debugger.Stacktrace do """ defmodule Frame do - defstruct [:level, :file, :module, :function, :args, :line, :bindings] + defstruct [:level, :file, :module, :function, :args, :line, :bindings, :messages] def name(%__MODULE__{} = frame) do "#{inspect(frame.module)}.#{frame.function}/#{Enum.count(frame.args)}" @@ -17,6 +17,8 @@ defmodule ElixirLS.Debugger.Stacktrace do [{level, {module, function, args}} | backtrace_rest] = :int.meta(meta_pid, :backtrace, :all) + messages = :int.meta(meta_pid, :messages) + first_frame = %Frame{ level: level, module: module, @@ -24,7 +26,8 @@ defmodule ElixirLS.Debugger.Stacktrace do args: args, file: get_file(module), line: break_line(pid), - bindings: get_bindings(meta_pid, level) + bindings: get_bindings(meta_pid, level), + messages: messages } # If backtrace_rest is empty, calling stack_frames causes an exception @@ -44,7 +47,8 @@ defmodule ElixirLS.Debugger.Stacktrace do args: args, file: get_file(mod), line: line, - bindings: Enum.into(bindings, %{}) + bindings: Enum.into(bindings, %{}), + messages: messages } end end diff --git a/apps/elixir_ls_debugger/lib/debugger/variables.ex b/apps/elixir_ls_debugger/lib/debugger/variables.ex index e55614b31..360f93e80 100644 --- a/apps/elixir_ls_debugger/lib/debugger/variables.ex +++ b/apps/elixir_ls_debugger/lib/debugger/variables.ex @@ -7,16 +7,31 @@ defmodule ElixirLS.Debugger.Variables do def child_type(var) when is_map(var), do: :named def child_type(var) when is_bitstring(var), do: :indexed def child_type(var) when is_tuple(var), do: :indexed - def child_type(var) when is_list(var), do: :indexed + + def child_type(var) when is_list(var) do + if Keyword.keyword?(var) do + :named + else + :indexed + end + end + def child_type(_var), do: nil def children(var, start, count) when is_list(var) do start = start || 0 count = count || Enum.count(var) - var - |> Enum.slice(start, count) - |> with_index_as_name(start) + sliced = + var + |> Enum.slice(start, count) + + if Keyword.keyword?(var) do + sliced + else + sliced + |> with_index_as_name(start) + end end def children(var, start, count) when is_tuple(var) do @@ -90,7 +105,14 @@ defmodule ElixirLS.Debugger.Variables do def type(var) when is_float(var), do: "float" def type(var) when is_function(var), do: "function" def type(var) when is_integer(var), do: "integer" - def type(var) when is_list(var), do: "list" + + def type(var) when is_list(var) do + if Keyword.keyword?(var) and var != [] do + "keyword" + else + "list" + end + end def type(%name{}), do: "%#{inspect(name)}{}" diff --git a/apps/elixir_ls_debugger/test/debugger_test.exs b/apps/elixir_ls_debugger/test/debugger_test.exs index e6fd317e5..0f83e5df6 100644 --- a/apps/elixir_ls_debugger/test/debugger_test.exs +++ b/apps/elixir_ls_debugger/test/debugger_test.exs @@ -171,6 +171,13 @@ defmodule ElixirLS.Debugger.ServerTest do "namedVariables" => 1, "variablesReference" => vars_id }, + %{ + "expensive" => false, + "indexedVariables" => 0, + "name" => "process info", + "namedVariables" => _, + "variablesReference" => _ + }, %{ "expensive" => false, "indexedVariables" => 1, diff --git a/apps/elixir_ls_debugger/test/variables_test.exs b/apps/elixir_ls_debugger/test/variables_test.exs index f0b0b4a18..27b385b7c 100644 --- a/apps/elixir_ls_debugger/test/variables_test.exs +++ b/apps/elixir_ls_debugger/test/variables_test.exs @@ -39,6 +39,8 @@ defmodule ElixirLS.Debugger.VariablesTest do assert Variables.type([1]) == "list" assert Variables.type('asd') == "list" + assert Variables.type(abc: 123) == "keyword" + assert Variables.type(%{}) == "map" assert Variables.type(%{asd: 123}) == "map" assert Variables.type(%{"asd" => 123}) == "map" @@ -87,6 +89,8 @@ defmodule ElixirLS.Debugger.VariablesTest do assert Variables.num_children([1]) == 1 assert Variables.num_children('asd') == 3 + assert Variables.num_children(abc: 123) == 1 + assert Variables.num_children(%{}) == 0 assert Variables.num_children(%{asd: 123}) == 1 assert Variables.num_children(%{"asd" => 123}) == 1 @@ -104,6 +108,20 @@ defmodule ElixirLS.Debugger.VariablesTest do assert Variables.children('asd', 0, 10) == [{"0", 97}, {"1", 115}, {"2", 100}] end + test "keyword" do + assert Variables.children([abc: 123], 0, 10) == [abc: 123] + + assert Variables.children([abc1: 121, abc2: 122, abc3: 123, abc4: 124], 0, 2) == [ + abc1: 121, + abc2: 122 + ] + + assert Variables.children([abc1: 121, abc2: 122, abc3: 123, abc4: 124], 1, 2) == [ + abc2: 122, + abc3: 123 + ] + end + test "tuple" do assert Variables.children({}, 0, 10) == [] assert Variables.children({1}, 0, 10) == [{"0", 1}] From 7f37d59ffe7952d70ddc2f44100227d558c8ef6e Mon Sep 17 00:00:00 2001 From: Manos Emmanouilidis Date: Tue, 19 Apr 2022 11:07:55 +0300 Subject: [PATCH 009/125] Suggest an appropriate module name with the 'defmodule' snippet (#684) * Suggest an appropriate module name when auto-completing the 'defmodule' snippet * use processed file_path instead of raw file:// uri for determining appropriate module_names * add umbrella_app test to guard against future regressions * special case common Phoenix folders when suggesting module names * fix broken test Some plugin or setting in my editor trimmed extra spaces from the lines but in this case it cause a test to break by changing the cursor position of the auto-completion trigger --- .../language_server/providers/completion.ex | 79 ++++++++++- .../lib/language_server/server.ex | 3 +- .../test/providers/completion_test.exs | 131 ++++++++++++++++++ 3 files changed, 211 insertions(+), 2 deletions(-) diff --git a/apps/language_server/lib/language_server/providers/completion.ex b/apps/language_server/lib/language_server/providers/completion.ex index b50f3c1d6..611a3508d 100644 --- a/apps/language_server/lib/language_server/providers/completion.ex +++ b/apps/language_server/lib/language_server/providers/completion.ex @@ -565,7 +565,9 @@ defmodule ElixirLS.LanguageServer.Providers.Completion do completion end - if snippet = snippet_for({origin, name}, context) do + file_path = Keyword.get(options, :file_path) + + if snippet = snippet_for({origin, name}, Map.put(context, :file_path, file_path)) do %{completion | insert_text: snippet, kind: :snippet, label: name} else completion @@ -576,6 +578,12 @@ defmodule ElixirLS.LanguageServer.Providers.Completion do nil end + defp snippet_for({"Kernel", "defmodule"}, %{file_path: file_path}) when is_binary(file_path) do + # In a mix project the file_path can be something like "/some/code/path/project/lib/project/sub_path/my_file.ex" + # so we'll try to guess the appropriate module name from the path + "defmodule #{suggest_module_name(file_path)}$1 do\n\t$0\nend" + end + defp snippet_for(key, %{pipe_before?: true}) do # Get pipe-friendly version of snippet if available, otherwise fallback to standard Map.get(@pipe_func_snippets, key) || Map.get(@func_snippets, key) @@ -593,6 +601,75 @@ defmodule ElixirLS.LanguageServer.Providers.Completion do end end + def suggest_module_name(file_path) when is_binary(file_path) do + file_path + |> Path.split() + |> Enum.reverse() + |> do_suggest_module_name() + end + + defp do_suggest_module_name([]), do: nil + + defp do_suggest_module_name([filename | reversed_path]) do + filename + |> String.split(".") + |> case do + [file, "ex"] -> + do_suggest_module_name(reversed_path, [file], topmost_parent: "lib") + + [file, "exs"] -> + if String.ends_with?(file, "_test") do + do_suggest_module_name(reversed_path, [file], topmost_parent: "test") + else + nil + end + + _otherwise -> + nil + end + end + + defp do_suggest_module_name([dir | _rest], module_name_acc, topmost_parent: topmost) + when dir == topmost do + module_name_acc + |> Enum.map(&Macro.camelize/1) + |> Enum.join(".") + end + + defp do_suggest_module_name( + [probable_phoenix_dir | [project_web_dir | _] = rest], + module_name_acc, + opts + ) + when probable_phoenix_dir in [ + "controllers", + "views", + "channels", + "plugs", + "endpoints", + "sockets" + ] do + if String.ends_with?(project_web_dir, "_web") do + # by convention Phoenix doesn't use these folders as part of the module names + # for modules located inside them, so we'll try to do the same + do_suggest_module_name(rest, module_name_acc, opts) + else + # when not directly under the *_web folder however then we should make the folder + # part of the module's name + do_suggest_module_name(rest, [probable_phoenix_dir | module_name_acc], opts) + end + end + + defp do_suggest_module_name([dir_name | rest], module_name_acc, opts) do + do_suggest_module_name(rest, [dir_name | module_name_acc], opts) + end + + defp do_suggest_module_name([], _module_name_acc, _opts) do + # we went all the way up without ever encountering a 'lib' or a 'test' folder + # so we ignore the accumulated module name because it's probably wrong/useless + nil + end + def function_snippet(name, args, arity, opts) do snippets_supported? = Keyword.get(opts, :snippets_supported, false) trigger_signature? = Keyword.get(opts, :trigger_signature?, false) diff --git a/apps/language_server/lib/language_server/server.ex b/apps/language_server/lib/language_server/server.ex index 0f2d4c09b..e5e9d27c7 100644 --- a/apps/language_server/lib/language_server/server.ex +++ b/apps/language_server/lib/language_server/server.ex @@ -685,7 +685,8 @@ defmodule ElixirLS.LanguageServer.Server do tags_supported: tags_supported, signature_help_supported: signature_help_supported, locals_without_parens: locals_without_parens, - signature_after_complete: signature_after_complete + signature_after_complete: signature_after_complete, + file_path: SourceFile.path_from_uri(uri) ) end diff --git a/apps/language_server/test/providers/completion_test.exs b/apps/language_server/test/providers/completion_test.exs index 3d55f5340..f60b923de 100644 --- a/apps/language_server/test/providers/completion_test.exs +++ b/apps/language_server/test/providers/completion_test.exs @@ -908,6 +908,62 @@ defmodule ElixirLS.LanguageServer.Providers.CompletionTest do """ } end + + test "will suggest defmodule with module_name snippet when file path matches **/lib/**/*.ex" do + text = """ + defmod + # ^ + """ + + {line, char} = {0, 6} + + TestUtils.assert_has_cursor_char(text, line, char) + + assert {:ok, %{"items" => [first | _] = _items}} = + Completion.completion( + text, + line, + char, + @supports + |> Keyword.put( + :file_path, + "/some/path/my_project/lib/my_project/sub_folder/my_file.ex" + ) + ) + + assert %{ + "label" => "defmodule", + "insertText" => "defmodule MyProject.SubFolder.MyFile$1 do\n\t$0\nend" + } = first + end + + test "will suggest defmodule without module_name snippet when file path does not match expected patterns" do + text = """ + defmod + # ^ + """ + + {line, char} = {0, 6} + + TestUtils.assert_has_cursor_char(text, line, char) + + assert {:ok, %{"items" => [first | _] = _items}} = + Completion.completion( + text, + line, + char, + @supports + |> Keyword.put( + :file_path, + "/some/path/my_project/lib/my_project/sub_folder/my_file.heex" + ) + ) + + assert %{ + "label" => "defmodule", + "insertText" => "defmodule $1 do\n\t$0\nend" + } = first + end end describe "generic suggestions" do @@ -1053,4 +1109,79 @@ defmodule ElixirLS.LanguageServer.Providers.CompletionTest do assert insert_text =~ "if do\n\t" end end + + describe "suggest_module_name/1" do + import Completion, only: [suggest_module_name: 1] + + test "returns nil if current file_path is empty" do + assert nil == suggest_module_name("") + end + + test "returns nil if current file is not an .ex file" do + assert nil == suggest_module_name("some/path/lib/dir/file.heex") + end + + test "returns nil if current file is an .ex file but no lib folder exists in path" do + assert nil == suggest_module_name("some/path/not_lib/dir/file.ex") + end + + test "returns nil if current file is an *_test.exs file but no test folder exists in path" do + assert nil == suggest_module_name("some/path/not_test/dir/file_test.exs") + end + + test "returns an appropriate suggestion if file directly under lib" do + assert "MyProject" == suggest_module_name("some/path/my_project/lib/my_project.ex") + end + + test "returns an appropriate suggestion if file arbitrarily nested under lib/" do + assert "MyProject.Foo.Bar.Baz.MyFile" = + suggest_module_name("some/path/my_project/lib/my_project/foo/bar/baz/my_file.ex") + end + + test "returns an appropriate suggestion if file directly under test/" do + assert "MyProjectTest" == + suggest_module_name("some/path/my_project/test/my_project_test.exs") + end + + test "returns an appropriate suggestion if file arbitrarily nested under test" do + assert "MyProject.Foo.Bar.Baz.MyFileTest" == + suggest_module_name( + "some/path/my_project/test/my_project/foo/bar/baz/my_file_test.exs" + ) + end + + test "returns an appropriate suggestion if file is part of an umbrella project" do + assert "MySubApp.Foo.Bar.Baz" == + suggest_module_name( + "some/path/my_umbrella_project/apps/my_sub_app/lib/my_sub_app/foo/bar/baz.ex" + ) + end + + test "returns appropriate suggestions for modules nested under known phoenix dirs" do + [ + {"MyProjectWeb.MyController", "controllers/my_controller.ex"}, + {"MyProjectWeb.MyPlug", "plugs/my_plug.ex"}, + {"MyProjectWeb.MyView", "views/my_view.ex"}, + {"MyProjectWeb.MyChannel", "channels/my_channel.ex"}, + {"MyProjectWeb.MyEndpoint", "endpoints/my_endpoint.ex"}, + {"MyProjectWeb.MySocket", "sockets/my_socket.ex"} + ] + |> Enum.each(fn {expected_module_name, partial_path} -> + path = "some/path/my_project/lib/my_project_web/#{partial_path}" + assert expected_module_name == suggest_module_name(path) + end) + end + + test "uses known Phoenix dirs as part of a module's name if these are not located directly beneath the *_web folder" do + assert "MyProject.Controllers.MyController" == + suggest_module_name( + "some/path/my_project/lib/my_project/controllers/my_controller.ex" + ) + + assert "MyProjectWeb.SomeNestedDir.Controllers.MyController" == + suggest_module_name( + "some/path/my_project/lib/my_project_web/some_nested_dir/controllers/my_controller.ex" + ) + end + end end From 1e38db4c9dd9277dfffd9563286f652e3d617a5f Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Wed, 18 May 2022 17:47:39 +0200 Subject: [PATCH 010/125] improvements to document symbols provider --- .../providers/document_symbols.ex | 58 +++-- .../lib/language_server/providers/hover.ex | 1 + .../test/providers/document_symbols_test.exs | 232 ++++++++---------- 3 files changed, 138 insertions(+), 153 deletions(-) diff --git a/apps/language_server/lib/language_server/providers/document_symbols.ex b/apps/language_server/lib/language_server/providers/document_symbols.ex index 9bb4b3ae1..afc3f0ab4 100644 --- a/apps/language_server/lib/language_server/providers/document_symbols.ex +++ b/apps/language_server/lib/language_server/providers/document_symbols.ex @@ -15,10 +15,13 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbols do @defs [:def, :defp, :defmacro, :defmacrop, :defguard, :defguardp, :defdelegate] - @docs [ + @supplementing_attributes [ :doc, :moduledoc, - :typedoc + :typedoc, + :spec, + :impl, + :deprecated ] @max_parser_errors 6 @@ -118,14 +121,8 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbols do end # Struct and exception - defp extract_symbol(_module_name, {defname, location, [properties | _]}) + defp extract_symbol(module_name, {defname, location, [properties | _]}) when defname in [:defstruct, :defexception] do - name = - case defname do - :defstruct -> "struct" - :defexception -> "exception" - end - children = if is_list(properties) do properties @@ -135,15 +132,20 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbols do [] end - %Info{type: :struct, name: name, location: location, children: children} + %Info{ + type: :struct, + name: "#{defname} #{module_name}", + location: location, + children: children + } end - # Docs - defp extract_symbol(_, {:@, _, [{kind, _, _}]}) when kind in @docs, do: nil + # We skip attributes only supplementoing the symbol + defp extract_symbol(_, {:@, _, [{kind, _, _}]}) when kind in @supplementing_attributes, do: nil # Types defp extract_symbol(_current_module, {:@, _, [{type_kind, location, type_expression}]}) - when type_kind in [:type, :typep, :opaque, :spec, :callback, :macrocallback] do + when type_kind in [:type, :typep, :opaque, :callback, :macrocallback] do type_name = case type_expression do [{:"::", _, [{_, _, _} = type_head | _]}] -> @@ -158,7 +160,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbols do %Info{ type: type, - name: type_name, + name: "@#{type_kind} #{type_name}", location: location, children: [] } @@ -168,19 +170,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbols do defp extract_symbol(_current_module, {:@, _, [{:behaviour, location, [behaviour_expression]}]}) do module_name = extract_module_name(behaviour_expression) - %Info{type: :constant, name: "@behaviour #{module_name}", location: location, children: []} - end - - # @impl true - defp extract_symbol(_current_module, {:@, _, [{:impl, location, [true]}]}) do - %Info{type: :constant, name: "@impl true", location: location, children: []} - end - - # @impl BehaviourModule - defp extract_symbol(_current_module, {:@, _, [{:impl, location, [impl_expression]}]}) do - module_name = extract_module_name(impl_expression) - - %Info{type: :constant, name: "@impl #{module_name}", location: location, children: []} + %Info{type: :interface, name: "@behaviour #{module_name}", location: location, children: []} end # Other attributes @@ -217,6 +207,20 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbols do } end + defp extract_symbol( + _current_module, + {{:., location, [{:__aliases__, _, [:Record]}, :defrecord]}, _, [record_name, _]} + ) do + name = Macro.to_string(record_name) |> String.replace("\n", "") + + %Info{ + type: :class, + name: "defrecord #{name}", + location: location, + children: [] + } + end + # ExUnit test defp extract_symbol(_current_module, {:test, location, [name | _]}) do %Info{ diff --git a/apps/language_server/lib/language_server/providers/hover.ex b/apps/language_server/lib/language_server/providers/hover.ex index cf2bd75ec..7ebd1bf0e 100644 --- a/apps/language_server/lib/language_server/providers/hover.ex +++ b/apps/language_server/lib/language_server/providers/hover.ex @@ -140,6 +140,7 @@ defmodule ElixirLS.LanguageServer.Providers.Hover do "" else s = root_mod_name |> source() + cond do third_dep?(s, project_dir) -> third_dep_name(s, project_dir) builtin?(s) -> builtin_dep_name(s) diff --git a/apps/language_server/test/providers/document_symbols_test.exs b/apps/language_server/test/providers/document_symbols_test.exs index 67948439f..293c9bb3a 100644 --- a/apps/language_server/test/providers/document_symbols_test.exs +++ b/apps/language_server/test/providers/document_symbols_test.exs @@ -1173,7 +1173,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do } ], kind: 23, - name: "struct", + name: "defstruct MyModule", range: %{ "end" => %{"character" => 2, "line" => 1}, "start" => %{"character" => 2, "line" => 1} @@ -1220,7 +1220,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do } }, %Protocol.SymbolInformation{ - name: "struct", + name: "defstruct MyModule", kind: 23, location: %{ range: %{ @@ -1239,7 +1239,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do "start" => %{"character" => 2, "line" => 1} } }, - containerName: "struct" + containerName: "defstruct MyModule" }, %Protocol.SymbolInformation{ kind: 7, @@ -1250,7 +1250,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do "start" => %{"character" => 2, "line" => 1} } }, - containerName: "struct" + containerName: "defstruct MyModule" } ]} = DocumentSymbols.symbols(uri, text, false) end @@ -1285,7 +1285,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do } ], kind: 23, - name: "exception", + name: "defexception MyError", range: %{ "end" => %{"character" => 2, "line" => 1}, "start" => %{"character" => 2, "line" => 1} @@ -1333,7 +1333,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do }, %Protocol.SymbolInformation{ kind: 23, - name: "exception", + name: "defexception MyError", location: %{ range: %{ "end" => %{"character" => 2, "line" => 1}, @@ -1351,7 +1351,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do "start" => %{"character" => 2, "line" => 1} } }, - containerName: "exception" + containerName: "defexception MyError" } ]} = DocumentSymbols.symbols(uri, text, false) end @@ -1377,7 +1377,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do %{ children: [], kind: 5, - name: "my_simple", + name: "@type my_simple", range: %{ "end" => %{"character" => 3, "line" => 1}, "start" => %{"character" => 3, "line" => 1} @@ -1390,7 +1390,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do %Protocol.DocumentSymbol{ children: [], kind: 5, - name: "my_union", + name: "@type my_union", range: %{ "end" => %{"character" => 3, "line" => 2}, "start" => %{"character" => 3, "line" => 2} @@ -1403,7 +1403,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do %Protocol.DocumentSymbol{ children: [], kind: 5, - name: "my_simple_private", + name: "@typep my_simple_private", range: %{ "end" => %{"character" => 3, "line" => 3}, "start" => %{"character" => 3, "line" => 3} @@ -1416,7 +1416,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do %Protocol.DocumentSymbol{ children: [], kind: 5, - name: "my_simple_opaque", + name: "@opaque my_simple_opaque", range: %{ "end" => %{"character" => 3, "line" => 4}, "start" => %{"character" => 3, "line" => 4} @@ -1429,7 +1429,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do %Protocol.DocumentSymbol{ children: [], kind: 5, - name: "my_with_args(key, value)", + name: "@type my_with_args(key, value)", range: %{ "end" => %{"character" => 3, "line" => 5}, "start" => %{"character" => 3, "line" => 5} @@ -1442,7 +1442,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do %Protocol.DocumentSymbol{ children: [], kind: 5, - name: "my_with_args_when(key, value)", + name: "@type my_with_args_when(key, value)", range: %{ "end" => %{"character" => 3, "line" => 6}, "start" => %{"character" => 3, "line" => 6} @@ -1495,7 +1495,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do }, %Protocol.SymbolInformation{ kind: 5, - name: "my_simple", + name: "@type my_simple", location: %{ range: %{ "end" => %{"character" => 3, "line" => 1}, @@ -1506,7 +1506,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do }, %Protocol.SymbolInformation{ kind: 5, - name: "my_union", + name: "@type my_union", location: %{ range: %{ "end" => %{"character" => 3, "line" => 2}, @@ -1517,7 +1517,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do }, %Protocol.SymbolInformation{ kind: 5, - name: "my_simple_private", + name: "@typep my_simple_private", location: %{ range: %{ "end" => %{"character" => 3, "line" => 3}, @@ -1528,7 +1528,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do }, %Protocol.SymbolInformation{ kind: 5, - name: "my_simple_opaque", + name: "@opaque my_simple_opaque", location: %{ range: %{ "end" => %{"character" => 3, "line" => 4}, @@ -1539,7 +1539,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do }, %Protocol.SymbolInformation{ kind: 5, - name: "my_with_args(key, value)", + name: "@type my_with_args(key, value)", location: %{ range: %{ "end" => %{"character" => 3, "line" => 5}, @@ -1550,7 +1550,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do }, %Protocol.SymbolInformation{ kind: 5, - name: "my_with_args_when(key, value)", + name: "@type my_with_args_when(key, value)", location: %{ range: %{ "end" => %{"character" => 3, "line" => 6}, @@ -1585,7 +1585,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do %Protocol.DocumentSymbol{ children: [], kind: 24, - name: "my_callback(type1, type2)", + name: "@callback my_callback(type1, type2)", range: %{ "end" => %{"character" => 3, "line" => 1}, "start" => %{"character" => 3, "line" => 1} @@ -1598,7 +1598,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do %Protocol.DocumentSymbol{ children: [], kind: 24, - name: "my_macrocallback(type1, type2)", + name: "@macrocallback my_macrocallback(type1, type2)", range: %{ "end" => %{"character" => 3, "line" => 2}, "start" => %{"character" => 3, "line" => 2} @@ -1611,7 +1611,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do %Protocol.DocumentSymbol{ children: [], kind: 24, - name: "my_callback_when(type1, type2)", + name: "@callback my_callback_when(type1, type2)", range: %{ "end" => %{"character" => 3, "line" => 4}, "start" => %{"character" => 3, "line" => 4} @@ -1624,7 +1624,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do %Protocol.DocumentSymbol{ children: [], kind: 24, - name: "my_macrocallback_when(type1, type2)", + name: "@macrocallback my_macrocallback_when(type1, type2)", range: %{ "end" => %{"character" => 3, "line" => 5}, "start" => %{"character" => 3, "line" => 5} @@ -1637,7 +1637,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do %Protocol.DocumentSymbol{ children: [], kind: 24, - name: "my_callback_no_arg()", + name: "@callback my_callback_no_arg()", range: %{ "end" => %{"character" => 3, "line" => 7}, "start" => %{"character" => 3, "line" => 7} @@ -1650,7 +1650,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do %Protocol.DocumentSymbol{ children: [], kind: 24, - name: "my_macrocallback_no_arg()", + name: "@macrocallback my_macrocallback_no_arg()", range: %{ "end" => %{"character" => 3, "line" => 8}, "start" => %{"character" => 3, "line" => 8} @@ -1704,7 +1704,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do } }, %Protocol.SymbolInformation{ - name: "my_callback(type1, type2)", + name: "@callback my_callback(type1, type2)", kind: 24, location: %{ range: %{ @@ -1715,7 +1715,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do containerName: "MyModule" }, %Protocol.SymbolInformation{ - name: "my_macrocallback(type1, type2)", + name: "@macrocallback my_macrocallback(type1, type2)", kind: 24, location: %{ range: %{ @@ -1726,7 +1726,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do containerName: "MyModule" }, %Protocol.SymbolInformation{ - name: "my_callback_when(type1, type2)", + name: "@callback my_callback_when(type1, type2)", kind: 24, location: %{ range: %{ @@ -1737,7 +1737,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do containerName: "MyModule" }, %Protocol.SymbolInformation{ - name: "my_macrocallback_when(type1, type2)", + name: "@macrocallback my_macrocallback_when(type1, type2)", kind: 24, location: %{ range: %{ @@ -1748,7 +1748,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do containerName: "MyModule" }, %Protocol.SymbolInformation{ - name: "my_callback_no_arg()", + name: "@callback my_callback_no_arg()", kind: 24, location: %{ range: %{ @@ -1759,7 +1759,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do containerName: "MyModule" }, %Protocol.SymbolInformation{ - name: "my_macrocallback_no_arg()", + name: "@macrocallback my_macrocallback_no_arg()", kind: 24, location: %{ range: %{ @@ -1785,19 +1785,6 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do [ %Protocol.DocumentSymbol{ children: [ - %Protocol.DocumentSymbol{ - children: [], - kind: 24, - name: "my_fn(integer)", - range: %{ - "end" => %{"character" => 9, "line" => 2}, - "start" => %{"character" => 9, "line" => 2} - }, - selectionRange: %{ - "end" => %{"character" => 9, "line" => 2}, - "start" => %{"character" => 9, "line" => 2} - } - }, %Protocol.DocumentSymbol{ children: [], kind: 12, @@ -1848,23 +1835,88 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do } }, %Protocol.SymbolInformation{ - name: "my_fn(integer)", - kind: 24, + name: "def my_fn(a)", + kind: 12, location: %{ range: %{ - "end" => %{"character" => 9, "line" => 2}, - "start" => %{"character" => 9, "line" => 2} + "end" => %{"character" => 12, "line" => 3}, + "start" => %{"character" => 12, "line" => 3} } }, containerName: "MyModule" + } + ]} = DocumentSymbols.symbols(uri, text, false) + end + + test "[nested] handles records" do + uri = "file:///project/file.ex" + text = ~S[ + defmodule MyModule do + require Record + Record.defrecord(:user, name: "meg", age: "25") + end + ] + + assert {:ok, + [ + %Protocol.DocumentSymbol{ + children: [ + %Protocol.DocumentSymbol{ + children: [], + kind: 5, + name: "defrecord :user", + range: %{ + "end" => %{"character" => 14, "line" => 3}, + "start" => %{"character" => 14, "line" => 3} + }, + selectionRange: %{ + "end" => %{"character" => 14, "line" => 3}, + "start" => %{"character" => 14, "line" => 3} + } + } + ], + kind: 2, + name: "MyModule", + range: %{ + "end" => %{"character" => 6, "line" => 1}, + "start" => %{"character" => 6, "line" => 1} + }, + selectionRange: %{ + "end" => %{"character" => 6, "line" => 1}, + "start" => %{"character" => 6, "line" => 1} + } + } + ]} = DocumentSymbols.symbols(uri, text, true) + end + + test "[flat] handles records" do + uri = "file:///project/file.ex" + text = ~S[ + defmodule MyModule do + require Record + Record.defrecord(:user, name: "meg", age: "25") + end + ] + + assert {:ok, + [ + %Protocol.SymbolInformation{ + name: "MyModule", + kind: 2, + location: %{ + range: %{ + "end" => %{"character" => 6, "line" => 1}, + "start" => %{"character" => 6, "line" => 1} + } + } }, %Protocol.SymbolInformation{ - name: "def my_fn(a)", - kind: 12, + name: "defrecord :user", + kind: 5, location: %{ range: %{ - "end" => %{"character" => 12, "line" => 3}, - "start" => %{"character" => 12, "line" => 3} + "end" => %{"character" => 14, "line" => 3}, + "start" => %{"character" => 14, "line" => 3} } }, containerName: "MyModule" @@ -1971,7 +2023,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do }, %Protocol.DocumentSymbol{ children: [], - kind: 14, + kind: 11, name: "@behaviour MyBehaviour", range: %{ "end" => %{"character" => 3, "line" => 2}, @@ -1982,19 +2034,6 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do "start" => %{"character" => 3, "line" => 2} } }, - %Protocol.DocumentSymbol{ - children: [], - kind: 14, - name: "@impl true", - range: %{ - "end" => %{"character" => 3, "line" => 3}, - "start" => %{"character" => 3, "line" => 3} - }, - selectionRange: %{ - "end" => %{"character" => 3, "line" => 3}, - "start" => %{"character" => 3, "line" => 3} - } - }, %Protocol.DocumentSymbol{ children: [], kind: 14, @@ -2034,19 +2073,6 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do "start" => %{"character" => 3, "line" => 6} } }, - %Protocol.DocumentSymbol{ - children: [], - kind: 14, - name: "@deprecated", - range: %{ - "end" => %{"character" => 3, "line" => 7}, - "start" => %{"character" => 3, "line" => 7} - }, - selectionRange: %{ - "end" => %{"character" => 3, "line" => 7}, - "start" => %{"character" => 3, "line" => 7} - } - }, %Protocol.DocumentSymbol{ children: [], kind: 14, @@ -2163,19 +2189,6 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do "end" => %{"character" => 3, "line" => 16}, "start" => %{"character" => 3, "line" => 16} } - }, - %Protocol.DocumentSymbol{ - children: [], - kind: 14, - name: "@impl MyBehaviour", - range: %{ - "end" => %{"character" => 3, "line" => 17}, - "start" => %{"character" => 3, "line" => 17} - }, - selectionRange: %{ - "end" => %{"character" => 3, "line" => 17}, - "start" => %{"character" => 3, "line" => 17} - } } ], kind: 2, @@ -2242,7 +2255,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do }, %Protocol.SymbolInformation{ name: "@behaviour MyBehaviour", - kind: 14, + kind: 11, location: %{ range: %{ "end" => %{"character" => 3, "line" => 2}, @@ -2251,17 +2264,6 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do }, containerName: "MyModule" }, - %Protocol.SymbolInformation{ - name: "@impl true", - kind: 14, - location: %{ - range: %{ - "end" => %{"character" => 3, "line" => 3}, - "start" => %{"character" => 3, "line" => 3} - } - }, - containerName: "MyModule" - }, %Protocol.SymbolInformation{ name: "@derive", kind: 14, @@ -2295,17 +2297,6 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do }, containerName: "MyModule" }, - %Protocol.SymbolInformation{ - name: "@deprecated", - kind: 14, - location: %{ - range: %{ - "end" => %{"character" => 3, "line" => 7}, - "start" => %{"character" => 3, "line" => 7} - } - }, - containerName: "MyModule" - }, %Protocol.SymbolInformation{ name: "@dialyzer", kind: 14, @@ -2404,17 +2395,6 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do } }, containerName: "MyModule" - }, - %Protocol.SymbolInformation{ - name: "@impl MyBehaviour", - kind: 14, - location: %{ - range: %{ - "end" => %{"character" => 3, "line" => 17}, - "start" => %{"character" => 3, "line" => 17} - } - }, - containerName: "MyModule" } ]} = DocumentSymbols.symbols(uri, text, false) end From 71fb6fc386158e89863756064c7756bcb3b588c7 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Wed, 18 May 2022 17:50:13 +0200 Subject: [PATCH 011/125] bump otp on ci --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 08aa879b9..f617d6b3e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,7 +34,7 @@ jobs: check_unused_deps: true warnings_as_errors: false - elixir: 1.13.x - otp: 24.1.x + otp: 24.3.x tests_may_fail: false # Needs to be false until https://github.com/elixir-lsp/elixir-ls/issues/642 is fixed warnings_as_errors: false @@ -67,7 +67,7 @@ jobs: matrix: include: - elixir: 1.13.x - otp: 24.1.x + otp: 24.3.x steps: - uses: actions/checkout@v2 - uses: erlef/setup-beam@v1 From d57b41fc128863d9ca0aa4d9001ba5b9d6767d7d Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Wed, 18 May 2022 17:52:54 +0200 Subject: [PATCH 012/125] unlock otp versions --- .github/workflows/ci.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f617d6b3e..cd52ae041 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,19 +17,19 @@ jobs: matrix: include: - elixir: 1.10.x - otp: 22.3.4.20 + otp: 22.3.4.x tests_may_fail: false check_unused_deps: true - elixir: 1.11.x - otp: 23.3.4.4 + otp: 23.3.4.x tests_may_fail: false check_unused_deps: true - elixir: 1.12.x - otp: 23.3.4.4 + otp: 23.3.4.x tests_may_fail: false check_unused_deps: true - elixir: 1.13.x - otp: 22.3.4.20 + otp: 22.3.4.x tests_may_fail: false check_unused_deps: true warnings_as_errors: false From c1a9a5fb33120d2deca31bec2d26d085d6f500b4 Mon Sep 17 00:00:00 2001 From: Timothy Vanderaerden Date: Wed, 18 May 2022 17:57:10 +0200 Subject: [PATCH 013/125] Misc mkdocs fixes (#692) * Consistent headers (getting-started section) * Fix wrong header for vscode getting-started page * Removes trailing spaces * Fixes typos --- docs/getting-started/neovim.md | 4 +++- docs/getting-started/vscode.md | 2 +- guides/initialization.md | 16 ++++++++-------- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/docs/getting-started/neovim.md b/docs/getting-started/neovim.md index df6583400..959ecec93 100644 --- a/docs/getting-started/neovim.md +++ b/docs/getting-started/neovim.md @@ -1,4 +1,6 @@ -# Setup +# NeoVim + +## Setup There are several plugins available for NeoVim: diff --git a/docs/getting-started/vscode.md b/docs/getting-started/vscode.md index b062513f3..7c110530f 100644 --- a/docs/getting-started/vscode.md +++ b/docs/getting-started/vscode.md @@ -1,4 +1,4 @@ -# Emacs +# VSCode ## Setup diff --git a/guides/initialization.md b/guides/initialization.md index 6b66f5774..0705f7659 100644 --- a/guides/initialization.md +++ b/guides/initialization.md @@ -4,10 +4,10 @@ When launching the elixir_ls server using the scripts, the initialization steps 1. Replace default IO with Json RPC notifications 2. Starts Mix -3. Starts the :language_server application +3. Starts the :language_server application 4. Overrides default Mix.Shell 5. Ensure the Hex version is accepted -6. Start receiving requet/responses +6. Start receiving requests/responses 7. Gets the "initialize" request 8. Starts building/analyzing the project @@ -23,9 +23,9 @@ The servers delegate the callbacks to the module `ElixirLS.LanguageServer.JsonRp ElixirLS uses standard Mix tooling. It will need this to retrieve project configuration, dependencies and so on. It heavily uses private Mix APIs here to start and retrieve paths for dependencies, archives and so on. -## Starts the :language_server application +## Starts the :language_server application -The main entry point for the language server is its application module `ElixirLS.LanguageServer`. It starts a supervisor with a few children. +The main entry point for the language server is its application module `ElixirLS.LanguageServer`. It starts a supervisor with a few children. The first one is the server itself `ElixirLS.LanguageServer.Server`. This is a named `GenServer` that holds the state for the project. That state has things like: @@ -47,7 +47,7 @@ Mix might have some "yes or no" questions that would not be possible to reply in ## Ensure the Hex version is accepted -Before building the project and start answering requests from the client, it must ensure that the Hex version is correct. If it is not, it might have trouble building things. +Before building the project and start answering requests from the client, it must ensure that the Hex version is correct. If it is not, it might have trouble building things. This is also a private Mix API. @@ -112,11 +112,11 @@ When the language server needs to "reload" the project it must first: - compile only the project's `mix.exs` file. This is to ensure it is properly sourced and that it can use it for reading project metadata; - ensure that it does not override the server logger config; -This process is handled on the private function `reload_project/0`. One interesting trick it uses is that it sets a different build path using `Mix.ProjectStack.post_config/1` (which is a private API). This is the point where it will create a `.elixir_ls` directory on your project. +This process is handled on the private function `reload_project/0`. One interesting trick it uses is that it sets a different build path using `Mix.ProjectStack.post_config/1` (which is a private API). This is the point where it will create a `.elixir_ls` directory on your project. After reloading, the BEAM instance is free of old code and is ready to fetch/compile/analyze the project. -### Analyzing the project +### Analyzing the project Even after build is finished, things are not yet done. Both ElixirLS and ElixirSense need dialyzer information for some introspection. So, after build is done it sends a message that is handled at `ElixirLS.LanguageServer.Server.handle_build_result/3`. @@ -126,6 +126,6 @@ Here is the point in time where it will build the [PLT (a Persistent Lookup Tabl There are many things done in this module. It tries to be smart about analyzing only the modules that have changed. It does that by first checking the integrity of the PLT and then loading all modules from the PLT using `:dialyzer_plt.all_modules/1`. -If it finds that there is a difference, than it calculates this difference (using `MapSet`) to separate stale modules from non-stale. Then it delegates do the `ElixirLS.LanguageServer.Dialyzer.Analyzer` module for the proper analysis run. +If it finds that there is a difference, than it calculates this difference (using `MapSet`) to separate stale modules from non-stale. Then it delegates do the `ElixirLS.LanguageServer.Dialyzer.Analyzer` module for the proper analysis run. In the end it will publish a message that the analysis has finished, which will be delegated all the way back to the server module. There it handles the results accordingly and finally it will be ready to introspect code. From ae40661aa36d88d577314a954f98997a99fa5a8f Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Wed, 18 May 2022 19:07:02 +0200 Subject: [PATCH 014/125] do not consider changes in .elixir-ls and _build dirs when deciding id rebuild is necessary Fixes https://github.com/elixir-lsp/elixir-ls/issues/686 --- apps/language_server/lib/language_server/server.ex | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/language_server/lib/language_server/server.ex b/apps/language_server/lib/language_server/server.ex index e5e9d27c7..c9bf9261f 100644 --- a/apps/language_server/lib/language_server/server.ex +++ b/apps/language_server/lib/language_server/server.ex @@ -407,7 +407,11 @@ defmodule ElixirLS.LanguageServer.Server do Enum.any?(changes, fn %{"uri" => uri = "file:" <> _, "type" => type} -> path = SourceFile.path_from_uri(uri) - Path.extname(path) in (additional_watched_extensions ++ @default_watched_extensions) and + relative_path = Path.relative_to(path, state.project_dir) + first_path_segment = relative_path |> Path.split() |> hd + + first_path_segment not in [".elixir_ls", "_build"] and + Path.extname(path) in (additional_watched_extensions ++ @default_watched_extensions) and (type in [1, 3] or not Map.has_key?(state.source_files, uri) or state.source_files[uri].dirty?) end) From ff130f3325715e5fcf6a27a497010adebe8ddca8 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Wed, 25 May 2022 07:24:43 +0200 Subject: [PATCH 015/125] Fix many cases of invalid Mix.Compiler.Diagnostic position handling do not return negative numbers as those are forbidden by the LSP properly treat 0 as unknown position add missing handling for positions with column add missing handling for positions as ranges if file the diagnostics refer to is not open fall back to loading it from the file system do not return lines and columns if we cannot load the file Fixes #695 --- .../lib/language_server/build.ex | 81 +++++++++++++++---- .../lib/language_server/server.ex | 18 ++++- apps/language_server/test/dialyzer_test.exs | 40 ++++----- 3 files changed, 103 insertions(+), 36 deletions(-) diff --git a/apps/language_server/lib/language_server/build.ex b/apps/language_server/lib/language_server/build.ex index e8d050cc2..7830c657f 100644 --- a/apps/language_server/lib/language_server/build.ex +++ b/apps/language_server/lib/language_server/build.ex @@ -108,7 +108,8 @@ defmodule ElixirLS.LanguageServer.Build do %Mix.Task.Compiler.Diagnostic{ compiler_name: "ElixirLS", file: Path.absname(System.get_env("MIX_EXS") || "mix.exs"), - position: nil, + # 0 means unknown + position: 0, message: msg, severity: :error, details: error @@ -278,17 +279,15 @@ defmodule ElixirLS.LanguageServer.Build do :ok end - defp range(position, nil) when is_integer(position) do - line = position - 1 - - # we don't care about utf16 positions here as we send 0 - %{ - "start" => %{"line" => line, "character" => 0}, - "end" => %{"line" => line, "character" => 0} - } - end + # for details see + # https://hexdocs.pm/mix/1.13.4/Mix.Task.Compiler.Diagnostic.html#t:position/0 + # https://microsoft.github.io/language-server-protocol/specifications/specification-3-16/#diagnostic - defp range(position, source_file) when is_integer(position) do + # position is a 1 based line number + # we return a range of trimmed text in that line + defp range(position, source_file) + when is_integer(position) and position >= 1 and is_binary(source_file) do + # line is 1 based line = position - 1 text = Enum.at(SourceFile.lines(source_file), line) || "" @@ -307,12 +306,64 @@ defmodule ElixirLS.LanguageServer.Build do } end - defp range(_, nil) do - # we don't care about utf16 positions here as we send 0 - %{"start" => %{"line" => 0, "character" => 0}, "end" => %{"line" => 0, "character" => 0}} + # position is a 1 based line number and 0 based character cursor (UTF8) + # we return a 0 length range exactly at that location + defp range({line_start, char_start}, source_file) + when line_start >= 1 and is_binary(source_file) do + lines = SourceFile.lines(source_file) + # line is 1 based + start_line = Enum.at(lines, line_start - 1) + # SourceFile.elixir_character_to_lsp assumes char to be 1 based but it's 0 based bere + character = SourceFile.elixir_character_to_lsp(start_line, char_start + 1) + + %{ + "start" => %{ + "line" => line_start - 1, + "character" => character + }, + "end" => %{ + "line" => line_start - 1, + "character" => character + } + } end - defp range(_, source_file) do + # position is a range defined by 1 based line numbers and 0 based character cursors (UTF8) + # we return exactly that range + defp range({line_start, char_start, line_end, char_end}, source_file) + when line_start >= 1 and line_end >= 1 and is_binary(source_file) do + lines = SourceFile.lines(source_file) + # line is 1 based + start_line = Enum.at(lines, line_start - 1) + end_line = Enum.at(lines, line_end - 1) + + # SourceFile.elixir_character_to_lsp assumes char to be 1 based but it's 0 based bere + start_char = SourceFile.elixir_character_to_lsp(start_line, char_start + 1) + end_char = SourceFile.elixir_character_to_lsp(end_line, char_end + 1) + + %{ + "start" => %{ + "line" => line_start - 1, + "character" => start_char + }, + "end" => %{ + "line" => line_end - 1, + "character" => end_char + } + } + end + + # position is 0 which means unknown + # we return the full file range + defp range(0, source_file) when is_binary(source_file) do SourceFile.full_range(source_file) end + + # source file is unknown + # we discard any position information as it is meaningless + # unfortunately LSP does not allow `null` range so we need to return something + defp range(_, nil) do + # we don't care about utf16 positions here as we send 0 + %{"start" => %{"line" => 0, "character" => 0}, "end" => %{"line" => 0, "character" => 0}} + end end diff --git a/apps/language_server/lib/language_server/server.ex b/apps/language_server/lib/language_server/server.ex index c9bf9261f..d79aaad27 100644 --- a/apps/language_server/lib/language_server/server.ex +++ b/apps/language_server/lib/language_server/server.ex @@ -1043,13 +1043,29 @@ defmodule ElixirLS.LanguageServer.Server do Dialyzer.check_support() == :ok and build_enabled?(state) and state.dialyzer_sup != nil end + defp safely_read_file(file) do + case File.read(file) do + {:ok, text} -> + text + + {:error, reason} -> + IO.warn("Couldn't read file #{file}: #{inspect(reason)}") + nil + end + end + defp publish_diagnostics(new_diagnostics, old_diagnostics, source_files) do files = Enum.uniq(Enum.map(new_diagnostics, & &1.file) ++ Enum.map(old_diagnostics, & &1.file)) for file <- files, uri = SourceFile.path_to_uri(file), - do: Build.publish_file_diagnostics(uri, new_diagnostics, Map.get(source_files, uri)) + do: + Build.publish_file_diagnostics( + uri, + new_diagnostics, + Map.get_lazy(source_files, uri, fn -> safely_read_file(file) end) + ) end defp show_version_warnings do diff --git a/apps/language_server/test/dialyzer_test.exs b/apps/language_server/test/dialyzer_test.exs index 082a3f06b..fa4d516af 100644 --- a/apps/language_server/test/dialyzer_test.exs +++ b/apps/language_server/test/dialyzer_test.exs @@ -41,8 +41,8 @@ defmodule ElixirLS.LanguageServer.DialyzerTest do %{ "message" => error_message1, "range" => %{ - "end" => %{"character" => 0, "line" => _}, - "start" => %{"character" => 0, "line" => _} + "end" => %{"character" => 12, "line" => 1}, + "start" => %{"character" => 2, "line" => 1} }, "severity" => 2, "source" => "ElixirLS Dialyzer" @@ -50,8 +50,8 @@ defmodule ElixirLS.LanguageServer.DialyzerTest do %{ "message" => error_message2, "range" => %{ - "end" => %{"character" => 0, "line" => _}, - "start" => %{"character" => 0, "line" => _} + "end" => %{"character" => 17, "line" => 2}, + "start" => %{"character" => 4, "line" => 2} }, "severity" => 2, "source" => "ElixirLS Dialyzer" @@ -175,8 +175,8 @@ defmodule ElixirLS.LanguageServer.DialyzerTest do %{ "message" => error_message1, "range" => %{ - "end" => %{"character" => 0, "line" => _}, - "start" => %{"character" => 0, "line" => _} + "end" => %{"character" => 12, "line" => 1}, + "start" => %{"character" => 2, "line" => 1} }, "severity" => 2, "source" => "ElixirLS Dialyzer" @@ -184,8 +184,8 @@ defmodule ElixirLS.LanguageServer.DialyzerTest do %{ "message" => error_message2, "range" => %{ - "end" => %{"character" => 0, "line" => _}, - "start" => %{"character" => 0, "line" => _} + "end" => %{"character" => 17, "line" => 2}, + "start" => %{"character" => 4, "line" => 2} }, "severity" => 2, "source" => "ElixirLS Dialyzer" @@ -229,8 +229,8 @@ defmodule ElixirLS.LanguageServer.DialyzerTest do %{ "message" => error_message1, "range" => %{ - "end" => %{"character" => 0, "line" => _}, - "start" => %{"character" => 0, "line" => _} + "end" => %{"character" => 12, "line" => 1}, + "start" => %{"character" => 2, "line" => 1} }, "severity" => 2, "source" => "ElixirLS Dialyzer" @@ -238,8 +238,8 @@ defmodule ElixirLS.LanguageServer.DialyzerTest do %{ "message" => error_message2, "range" => %{ - "end" => %{"character" => 0, "line" => _}, - "start" => %{"character" => 0, "line" => _} + "end" => %{"character" => 17, "line" => 2}, + "start" => %{"character" => 4, "line" => 2} }, "severity" => 2, "source" => "ElixirLS Dialyzer" @@ -274,8 +274,8 @@ defmodule ElixirLS.LanguageServer.DialyzerTest do %{ "message" => error_message1, "range" => %{ - "end" => %{"character" => 0, "line" => _}, - "start" => %{"character" => 0, "line" => _} + "end" => %{"character" => 12, "line" => 1}, + "start" => %{"character" => 2, "line" => 1} }, "severity" => 2, "source" => "ElixirLS Dialyzer" @@ -283,8 +283,8 @@ defmodule ElixirLS.LanguageServer.DialyzerTest do %{ "message" => _error_message2, "range" => %{ - "end" => %{"character" => 0, "line" => _}, - "start" => %{"character" => 0, "line" => _} + "end" => %{"character" => 17, "line" => 2}, + "start" => %{"character" => 4, "line" => 2} }, "severity" => 2, "source" => "ElixirLS Dialyzer" @@ -320,8 +320,8 @@ defmodule ElixirLS.LanguageServer.DialyzerTest do %{ "message" => error_message1, "range" => %{ - "end" => %{"character" => 0, "line" => _}, - "start" => %{"character" => 0, "line" => _} + "end" => %{"character" => 22, "line" => 1}, + "start" => %{"character" => 2, "line" => 1} }, "severity" => 2, "source" => "ElixirLS Dialyzer" @@ -329,8 +329,8 @@ defmodule ElixirLS.LanguageServer.DialyzerTest do %{ "message" => error_message2, "range" => %{ - "end" => %{"character" => 0, "line" => _}, - "start" => %{"character" => 0, "line" => _} + "end" => %{"character" => 22, "line" => 2}, + "start" => %{"character" => 4, "line" => 2} }, "severity" => 2, "source" => "ElixirLS Dialyzer" From 5af6a3f65bebef8d80b811730cdafffffd723af3 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Mon, 30 May 2022 18:08:12 +0200 Subject: [PATCH 016/125] fix crash introduced in ff130f3325715e5fcf6a27a497010adebe8ddca8 --- apps/language_server/lib/language_server/build.ex | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/language_server/lib/language_server/build.ex b/apps/language_server/lib/language_server/build.ex index 7830c657f..e40616da0 100644 --- a/apps/language_server/lib/language_server/build.ex +++ b/apps/language_server/lib/language_server/build.ex @@ -286,7 +286,7 @@ defmodule ElixirLS.LanguageServer.Build do # position is a 1 based line number # we return a range of trimmed text in that line defp range(position, source_file) - when is_integer(position) and position >= 1 and is_binary(source_file) do + when is_integer(position) and position >= 1 and not is_nil(source_file) do # line is 1 based line = position - 1 text = Enum.at(SourceFile.lines(source_file), line) || "" @@ -309,7 +309,7 @@ defmodule ElixirLS.LanguageServer.Build do # position is a 1 based line number and 0 based character cursor (UTF8) # we return a 0 length range exactly at that location defp range({line_start, char_start}, source_file) - when line_start >= 1 and is_binary(source_file) do + when line_start >= 1 and not is_nil(source_file) do lines = SourceFile.lines(source_file) # line is 1 based start_line = Enum.at(lines, line_start - 1) @@ -331,7 +331,7 @@ defmodule ElixirLS.LanguageServer.Build do # position is a range defined by 1 based line numbers and 0 based character cursors (UTF8) # we return exactly that range defp range({line_start, char_start, line_end, char_end}, source_file) - when line_start >= 1 and line_end >= 1 and is_binary(source_file) do + when line_start >= 1 and line_end >= 1 and not is_nil(source_file) do lines = SourceFile.lines(source_file) # line is 1 based start_line = Enum.at(lines, line_start - 1) @@ -355,7 +355,7 @@ defmodule ElixirLS.LanguageServer.Build do # position is 0 which means unknown # we return the full file range - defp range(0, source_file) when is_binary(source_file) do + defp range(0, source_file) when not is_nil(source_file) do SourceFile.full_range(source_file) end From 0da7623f644f79559699e9f002820ad9219d108d Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Mon, 30 May 2022 19:54:55 +0200 Subject: [PATCH 017/125] add support for OPT 25 dialyzer opts --- .../lib/language_server/dialyzer/analyzer.ex | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/apps/language_server/lib/language_server/dialyzer/analyzer.ex b/apps/language_server/lib/language_server/dialyzer/analyzer.ex index 4d15b6256..c3e708ed7 100644 --- a/apps/language_server/lib/language_server/dialyzer/analyzer.ex +++ b/apps/language_server/lib/language_server/dialyzer/analyzer.ex @@ -1,7 +1,15 @@ defmodule ElixirLS.LanguageServer.Dialyzer.Analyzer do require Record - # :warn_race_condition is unsupported because it greatly increases analysis time + # warn_race_condition is unsupported because it greatly increases analysis time + # OTP 25 dropped support for warn_race_condition + # see https://github.com/erlang/otp/commit/74c65fbb588b98ee24df9f7302a43552178dfac2 + # TODO remove this comment when OTP >= 25 is required + + # default warns taken from + # https://github.com/erlang/otp/blob/4ed7957623e5ccbd420a09a506bd6bc9930fe93c/lib/dialyzer/src/dialyzer_options.erl#L34 + # macros defined in https://github.com/erlang/otp/blob/4ed7957623e5ccbd420a09a506bd6bc9930fe93c/lib/dialyzer/src/dialyzer.hrl#L36 + # as of OTP 25 @default_warns [ :warn_behaviour, :warn_bin_construction, @@ -26,7 +34,17 @@ defmodule ElixirLS.LanguageServer.Dialyzer.Analyzer do :warn_return_only_exit, :warn_umatched_return, :warn_unknown - ] + ] ++ ( + if String.to_integer(System.otp_release()) >= 25 do + [ + # OTP >= 25 options + :warn_contract_missing_return, + :warn_contract_extra_return + ] + else + [] + end + ) @log_cache_length 10 defstruct [ From cef11ab5fb084a17fa8f7b8fd710fde7c7f97bc9 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Tue, 31 May 2022 20:22:41 +0200 Subject: [PATCH 018/125] bump elixir sense --- mix.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.lock b/mix.lock index 82bde6e0a..acc082fc4 100644 --- a/mix.lock +++ b/mix.lock @@ -1,7 +1,7 @@ %{ "dialyxir": {:hex, :dialyxir, "1.1.0", "c5aab0d6e71e5522e77beff7ba9e08f8e02bad90dfbeffae60eaf0cb47e29488", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "07ea8e49c45f15264ebe6d5b93799d4dd56a44036cf42d0ad9c960bc266c0b9a"}, "docsh": {:hex, :docsh, "0.7.2", "f893d5317a0e14269dd7fe79cf95fb6b9ba23513da0480ec6e77c73221cae4f2", [:rebar3], [{:providers, "1.8.1", [hex: :providers, repo: "hexpm", optional: false]}], "hexpm", "4e7db461bb07540d2bc3d366b8513f0197712d0495bb85744f367d3815076134"}, - "elixir_sense": {:git, "https://github.com/elixir-lsp/elixir_sense.git", "49e9d4ffffa6e97ee5acca29dded100e49a73ebb", []}, + "elixir_sense": {:git, "https://github.com/elixir-lsp/elixir_sense.git", "8a97e2b8f8cadef839da56c6b7e9bef91d332ce2", []}, "erl2ex": {:git, "https://github.com/dazuma/erl2ex.git", "244c2d9ed5805ef4855a491d8616b8842fef7ca4", []}, "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, "getopt": {:hex, :getopt, "1.0.1", "c73a9fa687b217f2ff79f68a3b637711bb1936e712b521d8ce466b29cbf7808a", [:rebar3], [], "hexpm", "53e1ab83b9ceb65c9672d3e7a35b8092e9bdc9b3ee80721471a161c10c59959c"}, From ca81cff54370ccb00b0e24a3378b44b682393f7e Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Tue, 31 May 2022 20:35:00 +0200 Subject: [PATCH 019/125] add OTP 25 to test matrix --- .github/workflows/ci.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cd52ae041..49dbb30ed 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,6 +33,11 @@ jobs: tests_may_fail: false check_unused_deps: true warnings_as_errors: false + - elixir: 1.13.x + otp: 25.x + tests_may_fail: false + check_unused_deps: true + warnings_as_errors: false - elixir: 1.13.x otp: 24.3.x tests_may_fail: false From d87d9bed4d19d8d2554b0a8a7062b9b160045d4d Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Tue, 31 May 2022 22:41:29 +0200 Subject: [PATCH 020/125] bump github actions --- .github/workflows/ci.yml | 6 +++--- .github/workflows/docsite.yml | 6 +++--- .github/workflows/release-asset.yml | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 49dbb30ed..52b4fb3e4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -49,7 +49,7 @@ jobs: env: MIX_ENV: test steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - uses: erlef/setup-beam@v1 with: otp-version: ${{matrix.otp}} @@ -74,13 +74,13 @@ jobs: - elixir: 1.13.x otp: 24.3.x steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - uses: erlef/setup-beam@v1 with: otp-version: ${{matrix.otp}} elixir-version: ${{matrix.elixir}} - name: Cache build artifacts - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: | ~/.hex diff --git a/.github/workflows/docsite.yml b/.github/workflows/docsite.yml index c7f083f25..075bd5cd2 100644 --- a/.github/workflows/docsite.yml +++ b/.github/workflows/docsite.yml @@ -15,7 +15,7 @@ jobs: image: squidfunk/mkdocs-material steps: - name: Checkout - uses: actions/checkout@v2.3.1 + uses: actions/checkout@v3 - name: Build run: mkdocs build -s - name: Upload artifact @@ -32,9 +32,9 @@ jobs: max-parallel: 1 steps: - name: Checkout - uses: actions/checkout@v2.3.1 + uses: actions/checkout@v3 - name: Download artifact - uses: actions/download-artifact@v1 + uses: actions/download-artifact@v3 with: name: site path: site diff --git a/.github/workflows/release-asset.yml b/.github/workflows/release-asset.yml index 831900b9c..57ca25070 100644 --- a/.github/workflows/release-asset.yml +++ b/.github/workflows/release-asset.yml @@ -43,7 +43,7 @@ jobs: default: true steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up BEAM uses: erlef/setup-beam@v1 with: @@ -51,7 +51,7 @@ jobs: otp-version: ${{ matrix.otp-version }} - name: Restore dependencies cache - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: deps key: ${{ runner.os }}-mix-${{ hashFiles('**/mix.lock') }} From 85cf3cc0697800ea1e1b5491519ac47ec783eb9d Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Wed, 1 Jun 2022 10:46:23 +0200 Subject: [PATCH 021/125] bump version --- apps/elixir_ls_debugger/mix.exs | 2 +- apps/elixir_ls_utils/mix.exs | 2 +- apps/language_server/mix.exs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/elixir_ls_debugger/mix.exs b/apps/elixir_ls_debugger/mix.exs index 034c0a154..c35adb135 100644 --- a/apps/elixir_ls_debugger/mix.exs +++ b/apps/elixir_ls_debugger/mix.exs @@ -4,7 +4,7 @@ defmodule ElixirLS.Debugger.Mixfile do def project do [ app: :elixir_ls_debugger, - version: "0.9.0", + version: "0.10.0", build_path: "../../_build", config_path: "../../config/config.exs", deps_path: "../../deps", diff --git a/apps/elixir_ls_utils/mix.exs b/apps/elixir_ls_utils/mix.exs index 52683fe8a..cc3487407 100644 --- a/apps/elixir_ls_utils/mix.exs +++ b/apps/elixir_ls_utils/mix.exs @@ -4,7 +4,7 @@ defmodule ElixirLS.Utils.Mixfile do def project do [ app: :elixir_ls_utils, - version: "0.9.0", + version: "0.10.0", build_path: "../../_build", config_path: "../../config/config.exs", deps_path: "../../deps", diff --git a/apps/language_server/mix.exs b/apps/language_server/mix.exs index 91db24f15..6daed43d3 100644 --- a/apps/language_server/mix.exs +++ b/apps/language_server/mix.exs @@ -4,7 +4,7 @@ defmodule ElixirLS.LanguageServer.Mixfile do def project do [ app: :language_server, - version: "0.9.0", + version: "0.10.0", elixir: ">= 1.10.0", build_path: "../../_build", config_path: "../../config/config.exs", From ec1b31c8c7116aa2998834fcc412ed85ee36f571 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Wed, 1 Jun 2022 11:00:28 +0200 Subject: [PATCH 022/125] fix md formatting --- DEVELOPMENT.md | 36 ++++++++++++++++++++---------------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index bffddb8d7..8b71ddb3f 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -1,4 +1,6 @@ -# Version Support Guidelines +# Development + +## Version Support Guidelines Elixir itself supports 5 versions with security updates: https://hexdocs.pm/elixir/compatibility-and-deprecations.html#content @@ -8,30 +10,32 @@ http://erlang.2086793.n4.nabble.com/OTP-Versions-and-Maint-Branches-td4722416.ht ElixirLS generally aims to support the last 3 versions of Elixir and the last 3 versions of OTP. However this is not a hard and fast rule and may change in the future. -# Packaging +## Packaging + +Follow those instuctions when publishing a new release. -Bump the changelog -Bump the version numbers in `apps/elixir_ls_debugger/mix.exs`, `apps/elixir_ls_utils/mix.exs`, and `apps/language_server/mix.exs` -Make PR -Merge PR -Pull down the latest master -Make the tag from the new master -Push the tag (`git push upstream --tags`) -Wait for github actions to push up a draft release https://github.com/elixir-lsp/elixir-ls/releases -Edit the draft release with a link to the changelog -Publish the draft release +1. Bump the changelog +2. Bump the version numbers in `apps/elixir_ls_debugger/mix.exs`, `apps/elixir_ls_utils/mix.exs`, and `apps/language_server/mix.exs` +3. Make PR +4. Merge PR +5. Pull down the latest master +6. Make the tag from the new master +7. Push the tag (`git push upstream --tags`) +8. Wait for github actions to push up a draft release https://github.com/elixir-lsp/elixir-ls/releases +9. Edit the draft release with a link to the changelog +10. Publish the draft release -# Debugging +## Debugging If you're debugging a running server than `IO.inspect` is a good approach, any messages you create with it will be sent to your LSP client as a log message To debug in tests you can use `IO.inspect(Process.whereis(:user), message, label: "message")` to send your output directly to the group leader of the test process. -# Documentation website +## Documentation website The documentation website is built using the [Mkdocs](https://www.mkdocs.org) static website generator. The content is written in Markdown format in the directory [docs](./docs) and is configured via the [mkdocs.yml](./mkdocs.yml) file. -## Development +### Development Make sure you have a recent version of Python 3 and [Pip](https://pip.readthedocs.io/en/stable/installing/) installed. @@ -43,6 +47,6 @@ pip install mkdocs mkdocs-material Once installed, simply run `mkdocs serve` from the project root. This will start a local web server with a file watcher. -## Build +### Build To compile the website for deployment, run `mkdocs build` from the project root. The built static assets will be located in the `site` directory. These can then be served by any web hosting solution. From c1004851c2676ce72be93eba913d11888bc43b19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Samson?= Date: Thu, 2 Jun 2022 12:19:44 +0200 Subject: [PATCH 023/125] Build cleanup (#698) * check if loading apps is still needed * Revert "check if loading apps is still needed" This reverts commit d03cfe562043805d55934bd770940a44cc27eb43. * rename function to match what it actually do * extract diagnostic related code * extract common mix.exs code * fix tests --- .../elixir_ls_debugger/lib/debugger/server.ex | 5 +- apps/elixir_ls_utils/lib/mixfile_helpers.ex | 5 + .../test/support/mix_test.case.ex | 3 +- .../lib/language_server/build.ex | 176 ++---------------- .../lib/language_server/diagnostics.ex | 158 +++++++++++++++- .../lib/language_server/server.ex | 16 +- .../test/providers/code_lens/test_test.exs | 2 +- .../test/providers/completion_test.exs | 2 +- 8 files changed, 190 insertions(+), 177 deletions(-) create mode 100644 apps/elixir_ls_utils/lib/mixfile_helpers.ex diff --git a/apps/elixir_ls_debugger/lib/debugger/server.ex b/apps/elixir_ls_debugger/lib/debugger/server.ex index edc78008c..50ab16250 100644 --- a/apps/elixir_ls_debugger/lib/debugger/server.ex +++ b/apps/elixir_ls_debugger/lib/debugger/server.ex @@ -25,6 +25,7 @@ defmodule ElixirLS.Debugger.Server do } alias ElixirLS.Debugger.Stacktrace.Frame + alias ElixirLS.Utils.MixfileHelpers use GenServer use Protocol @@ -955,11 +956,11 @@ defmodule ElixirLS.Debugger.Server do File.cd!(project_dir) # Mixfile may already be loaded depending on cwd when launching debugger task - mixfile = Path.absname(System.get_env("MIX_EXS") || "mix.exs") + mixfile = Path.absname(MixfileHelpers.mix_exs()) # FIXME: Private API unless match?(%{file: ^mixfile}, Mix.ProjectStack.peek()) do - Code.compile_file(System.get_env("MIX_EXS") || "mix.exs") + Code.compile_file(MixfileHelpers.mix_exs()) end task = task || Mix.Project.config()[:default_task] diff --git a/apps/elixir_ls_utils/lib/mixfile_helpers.ex b/apps/elixir_ls_utils/lib/mixfile_helpers.ex new file mode 100644 index 000000000..d8425df5b --- /dev/null +++ b/apps/elixir_ls_utils/lib/mixfile_helpers.ex @@ -0,0 +1,5 @@ +defmodule ElixirLS.Utils.MixfileHelpers do + def mix_exs do + System.get_env("MIX_EXS") || "mix.exs" + end +end diff --git a/apps/elixir_ls_utils/test/support/mix_test.case.ex b/apps/elixir_ls_utils/test/support/mix_test.case.ex index be4f5a84f..116f787ca 100644 --- a/apps/elixir_ls_utils/test/support/mix_test.case.ex +++ b/apps/elixir_ls_utils/test/support/mix_test.case.ex @@ -1,6 +1,7 @@ defmodule ElixirLS.Utils.MixTest.Case do # This module is based heavily on MixTest.Case in Elixir's tests use ExUnit.CaseTemplate + alias ElixirLS.Utils.MixfileHelpers using do quote do @@ -103,7 +104,7 @@ defmodule ElixirLS.Utils.MixTest.Case do clear_mix_cache() # Attempt to purge mixfiles for dependencies to avoid module redefinition warnings - mix_exs = System.get_env("MIX_EXS") || "mix.exs" + mix_exs = MixfileHelpers.mix_exs() for {mod, :in_memory} <- :code.all_loaded(), source = mod.module_info[:compile][:source], diff --git a/apps/language_server/lib/language_server/build.ex b/apps/language_server/lib/language_server/build.ex index e40616da0..de57c75d8 100644 --- a/apps/language_server/lib/language_server/build.ex +++ b/apps/language_server/lib/language_server/build.ex @@ -1,5 +1,6 @@ defmodule ElixirLS.LanguageServer.Build do - alias ElixirLS.LanguageServer.{Server, JsonRpc, SourceFile, Diagnostics} + alias ElixirLS.LanguageServer.{Server, JsonRpc, Diagnostics} + alias ElixirLS.Utils.MixfileHelpers def build(parent, root_path, opts) when is_binary(root_path) do if Path.absname(File.cwd!()) != Path.absname(root_path) do @@ -28,8 +29,8 @@ defmodule ElixirLS.LanguageServer.Build do purge_consolidated_protocols() {status, diagnostics} = compile() - if status in [:ok, :noop] and Keyword.get(opts, :load_all_modules?) do - load_all_modules() + if status in [:ok, :noop] and Keyword.get(opts, :load_all_mix_applications?) do + load_all_mix_applications() end diagnostics = Diagnostics.normalize(diagnostics, root_path) @@ -49,79 +50,12 @@ defmodule ElixirLS.LanguageServer.Build do end end - def publish_file_diagnostics(uri, all_diagnostics, source_file) do - diagnostics = - all_diagnostics - |> Enum.filter(&(SourceFile.path_to_uri(&1.file) == uri)) - |> Enum.sort_by(fn %{position: position} -> position end) - - diagnostics_json = - for diagnostic <- diagnostics do - severity = - case diagnostic.severity do - :error -> 1 - :warning -> 2 - :information -> 3 - :hint -> 4 - end - - message = - case diagnostic.message do - m when is_binary(m) -> m - m when is_list(m) -> m |> Enum.join("\n") - end - - %{ - "message" => message, - "severity" => severity, - "range" => range(diagnostic.position, source_file), - "source" => diagnostic.compiler_name - } - end - - JsonRpc.notify("textDocument/publishDiagnostics", %{ - "uri" => uri, - "diagnostics" => diagnostics_json - }) - end - - def mixfile_diagnostic({file, line, message}, severity) do - %Mix.Task.Compiler.Diagnostic{ - compiler_name: "ElixirLS", - file: file, - position: line, - message: message, - severity: severity - } - end - - def exception_to_diagnostic(error) do - msg = - case error do - {:shutdown, 1} -> - "Build failed for unknown reason. See output log." - - _ -> - Exception.format_exit(error) - end - - %Mix.Task.Compiler.Diagnostic{ - compiler_name: "ElixirLS", - file: Path.absname(System.get_env("MIX_EXS") || "mix.exs"), - # 0 means unknown - position: 0, - message: msg, - severity: :error, - details: error - } - end - def with_build_lock(func) do :global.trans({__MODULE__, self()}, func) end defp reload_project do - mixfile = Path.absname(System.get_env("MIX_EXS") || "mix.exs") + mixfile = Path.absname(MixfileHelpers.mix_exs) if File.exists?(mixfile) do # FIXME: Private API @@ -145,13 +79,13 @@ defmodule ElixirLS.LanguageServer.Build do {status, diagnostics} = case Kernel.ParallelCompiler.compile([mixfile]) do {:ok, _, warnings} -> - {:ok, Enum.map(warnings, &mixfile_diagnostic(&1, :warning))} + {:ok, Enum.map(warnings, &Diagnostics.mixfile_diagnostic(&1, :warning))} {:error, errors, warnings} -> { :error, - Enum.map(warnings, &mixfile_diagnostic(&1, :warning)) ++ - Enum.map(errors, &mixfile_diagnostic(&1, :error)) + Enum.map(warnings, &Diagnostics.mixfile_diagnostic(&1, :warning)) ++ + Enum.map(errors, &Diagnostics.mixfile_diagnostic(&1, :error)) } end @@ -174,7 +108,11 @@ defmodule ElixirLS.LanguageServer.Build do end end - def load_all_modules do + # TODO It looks like that function is no longer needed on elixir >= 1.11 + # it was added in https://github.com/elixir-lsp/elixir-ls/pull/227 + # removing it doesn't break tests and I'm not able to reproduce + # https://github.com/elixir-lsp/elixir-ls/issues/209 on recent elixir (1.13) + def load_all_mix_applications do apps = cond do Mix.Project.umbrella?() -> @@ -278,92 +216,4 @@ defmodule ElixirLS.LanguageServer.Build do :ok end - - # for details see - # https://hexdocs.pm/mix/1.13.4/Mix.Task.Compiler.Diagnostic.html#t:position/0 - # https://microsoft.github.io/language-server-protocol/specifications/specification-3-16/#diagnostic - - # position is a 1 based line number - # we return a range of trimmed text in that line - defp range(position, source_file) - when is_integer(position) and position >= 1 and not is_nil(source_file) do - # line is 1 based - line = position - 1 - text = Enum.at(SourceFile.lines(source_file), line) || "" - - start_idx = String.length(text) - String.length(String.trim_leading(text)) + 1 - length = max(String.length(String.trim(text)), 1) - - %{ - "start" => %{ - "line" => line, - "character" => SourceFile.elixir_character_to_lsp(text, start_idx) - }, - "end" => %{ - "line" => line, - "character" => SourceFile.elixir_character_to_lsp(text, start_idx + length) - } - } - end - - # position is a 1 based line number and 0 based character cursor (UTF8) - # we return a 0 length range exactly at that location - defp range({line_start, char_start}, source_file) - when line_start >= 1 and not is_nil(source_file) do - lines = SourceFile.lines(source_file) - # line is 1 based - start_line = Enum.at(lines, line_start - 1) - # SourceFile.elixir_character_to_lsp assumes char to be 1 based but it's 0 based bere - character = SourceFile.elixir_character_to_lsp(start_line, char_start + 1) - - %{ - "start" => %{ - "line" => line_start - 1, - "character" => character - }, - "end" => %{ - "line" => line_start - 1, - "character" => character - } - } - end - - # position is a range defined by 1 based line numbers and 0 based character cursors (UTF8) - # we return exactly that range - defp range({line_start, char_start, line_end, char_end}, source_file) - when line_start >= 1 and line_end >= 1 and not is_nil(source_file) do - lines = SourceFile.lines(source_file) - # line is 1 based - start_line = Enum.at(lines, line_start - 1) - end_line = Enum.at(lines, line_end - 1) - - # SourceFile.elixir_character_to_lsp assumes char to be 1 based but it's 0 based bere - start_char = SourceFile.elixir_character_to_lsp(start_line, char_start + 1) - end_char = SourceFile.elixir_character_to_lsp(end_line, char_end + 1) - - %{ - "start" => %{ - "line" => line_start - 1, - "character" => start_char - }, - "end" => %{ - "line" => line_end - 1, - "character" => end_char - } - } - end - - # position is 0 which means unknown - # we return the full file range - defp range(0, source_file) when not is_nil(source_file) do - SourceFile.full_range(source_file) - end - - # source file is unknown - # we discard any position information as it is meaningless - # unfortunately LSP does not allow `null` range so we need to return something - defp range(_, nil) do - # we don't care about utf16 positions here as we send 0 - %{"start" => %{"line" => 0, "character" => 0}, "end" => %{"line" => 0, "character" => 0}} - end end diff --git a/apps/language_server/lib/language_server/diagnostics.ex b/apps/language_server/lib/language_server/diagnostics.ex index d826ef802..2ec5f8d2a 100644 --- a/apps/language_server/lib/language_server/diagnostics.ex +++ b/apps/language_server/lib/language_server/diagnostics.ex @@ -1,5 +1,6 @@ defmodule ElixirLS.LanguageServer.Diagnostics do - alias ElixirLS.LanguageServer.SourceFile + alias ElixirLS.LanguageServer.{SourceFile, JsonRpc} + alias ElixirLS.Utils.MixfileHelpers def normalize(diagnostics, root_path) do for diagnostic <- diagnostics do @@ -178,4 +179,159 @@ defmodule ElixirLS.LanguageServer.Diagnostics do end end) end + + def publish_file_diagnostics(uri, all_diagnostics, source_file) do + diagnostics = + all_diagnostics + |> Enum.filter(&(SourceFile.path_to_uri(&1.file) == uri)) + |> Enum.sort_by(fn %{position: position} -> position end) + + diagnostics_json = + for diagnostic <- diagnostics do + severity = + case diagnostic.severity do + :error -> 1 + :warning -> 2 + :information -> 3 + :hint -> 4 + end + + message = + case diagnostic.message do + m when is_binary(m) -> m + m when is_list(m) -> m |> Enum.join("\n") + end + + %{ + "message" => message, + "severity" => severity, + "range" => range(diagnostic.position, source_file), + "source" => diagnostic.compiler_name + } + end + + JsonRpc.notify("textDocument/publishDiagnostics", %{ + "uri" => uri, + "diagnostics" => diagnostics_json + }) + end + + def mixfile_diagnostic({file, line, message}, severity) do + %Mix.Task.Compiler.Diagnostic{ + compiler_name: "ElixirLS", + file: file, + position: line, + message: message, + severity: severity + } + end + + def exception_to_diagnostic(error) do + msg = + case error do + {:shutdown, 1} -> + "Build failed for unknown reason. See output log." + + _ -> + Exception.format_exit(error) + end + + %Mix.Task.Compiler.Diagnostic{ + compiler_name: "ElixirLS", + file: Path.absname(MixfileHelpers.mix_exs), + # 0 means unknown + position: 0, + message: msg, + severity: :error, + details: error + } + end + + # for details see + # https://hexdocs.pm/mix/1.13.4/Mix.Task.Compiler.Diagnostic.html#t:position/0 + # https://microsoft.github.io/language-server-protocol/specifications/specification-3-16/#diagnostic + + # position is a 1 based line number + # we return a range of trimmed text in that line + defp range(position, source_file) + when is_integer(position) and position >= 1 and not is_nil(source_file) do + # line is 1 based + line = position - 1 + text = Enum.at(SourceFile.lines(source_file), line) || "" + + start_idx = String.length(text) - String.length(String.trim_leading(text)) + 1 + length = max(String.length(String.trim(text)), 1) + + %{ + "start" => %{ + "line" => line, + "character" => SourceFile.elixir_character_to_lsp(text, start_idx) + }, + "end" => %{ + "line" => line, + "character" => SourceFile.elixir_character_to_lsp(text, start_idx + length) + } + } + end + + # position is a 1 based line number and 0 based character cursor (UTF8) + # we return a 0 length range exactly at that location + defp range({line_start, char_start}, source_file) + when line_start >= 1 and not is_nil(source_file) do + lines = SourceFile.lines(source_file) + # line is 1 based + start_line = Enum.at(lines, line_start - 1) + # SourceFile.elixir_character_to_lsp assumes char to be 1 based but it's 0 based bere + character = SourceFile.elixir_character_to_lsp(start_line, char_start + 1) + + %{ + "start" => %{ + "line" => line_start - 1, + "character" => character + }, + "end" => %{ + "line" => line_start - 1, + "character" => character + } + } + end + + # position is a range defined by 1 based line numbers and 0 based character cursors (UTF8) + # we return exactly that range + defp range({line_start, char_start, line_end, char_end}, source_file) + when line_start >= 1 and line_end >= 1 and not is_nil(source_file) do + lines = SourceFile.lines(source_file) + # line is 1 based + start_line = Enum.at(lines, line_start - 1) + end_line = Enum.at(lines, line_end - 1) + + # SourceFile.elixir_character_to_lsp assumes char to be 1 based but it's 0 based bere + start_char = SourceFile.elixir_character_to_lsp(start_line, char_start + 1) + end_char = SourceFile.elixir_character_to_lsp(end_line, char_end + 1) + + %{ + "start" => %{ + "line" => line_start - 1, + "character" => start_char + }, + "end" => %{ + "line" => line_end - 1, + "character" => end_char + } + } + end + + # position is 0 which means unknown + # we return the full file range + defp range(0, source_file) when not is_nil(source_file) do + SourceFile.full_range(source_file) + end + + # source file is unknown + # we discard any position information as it is meaningless + # unfortunately LSP does not allow `null` range so we need to return something + defp range(_, nil) do + # we don't care about utf16 positions here as we send 0 + %{"start" => %{"line" => 0, "character" => 0}, "end" => %{"line" => 0, "character" => 0}} + end end diff --git a/apps/language_server/lib/language_server/server.ex b/apps/language_server/lib/language_server/server.ex index d79aaad27..9cfa998a6 100644 --- a/apps/language_server/lib/language_server/server.ex +++ b/apps/language_server/lib/language_server/server.ex @@ -16,7 +16,7 @@ defmodule ElixirLS.LanguageServer.Server do """ use GenServer - alias ElixirLS.LanguageServer.{SourceFile, Build, Protocol, JsonRpc, Dialyzer} + alias ElixirLS.LanguageServer.{SourceFile, Build, Protocol, JsonRpc, Dialyzer, Diagnostics} alias ElixirLS.LanguageServer.Providers.{ Completion, @@ -49,7 +49,7 @@ defmodule ElixirLS.LanguageServer.Server do build_diagnostics: [], dialyzer_diagnostics: [], needs_build?: false, - load_all_modules?: false, + load_all_mix_applications?: false, build_running?: false, analysis_ready?: false, received_shutdown?: false, @@ -232,7 +232,7 @@ defmodule ElixirLS.LanguageServer.Server do state = case reason do :normal -> state - _ -> handle_build_result(:error, [Build.exception_to_diagnostic(reason)], state) + _ -> handle_build_result(:error, [Diagnostics.exception_to_diagnostic(reason)], state) end if reason == :normal do @@ -332,7 +332,7 @@ defmodule ElixirLS.LanguageServer.Server do else source_file = %SourceFile{text: text, version: version} - Build.publish_file_diagnostics( + Diagnostics.publish_file_diagnostics( uri, state.build_diagnostics ++ state.dialyzer_diagnostics, source_file @@ -926,7 +926,7 @@ defmodule ElixirLS.LanguageServer.Server do {_pid, build_ref} = Build.build(self(), project_dir, fetch_deps?: fetch_deps?, - load_all_modules?: state.load_all_modules? + load_all_mix_applications?: state.load_all_mix_applications? ) %__MODULE__{ @@ -935,7 +935,7 @@ defmodule ElixirLS.LanguageServer.Server do needs_build?: false, build_running?: true, analysis_ready?: false, - load_all_modules?: false + load_all_mix_applications?: false } true -> @@ -1061,7 +1061,7 @@ defmodule ElixirLS.LanguageServer.Server do for file <- files, uri = SourceFile.path_to_uri(file), do: - Build.publish_file_diagnostics( + Diagnostics.publish_file_diagnostics( uri, new_diagnostics, Map.get_lazy(source_files, uri, fn -> safely_read_file(file) end) @@ -1217,7 +1217,7 @@ defmodule ElixirLS.LanguageServer.Server do is_nil(prev_project_dir) -> File.cd!(project_dir) - Map.merge(state, %{project_dir: File.cwd!(), load_all_modules?: true}) + Map.merge(state, %{project_dir: File.cwd!(), load_all_mix_applications?: true}) prev_project_dir != project_dir -> JsonRpc.show_message( diff --git a/apps/language_server/test/providers/code_lens/test_test.exs b/apps/language_server/test/providers/code_lens/test_test.exs index 6912dae40..c6e898fb1 100644 --- a/apps/language_server/test/providers/code_lens/test_test.exs +++ b/apps/language_server/test/providers/code_lens/test_test.exs @@ -7,7 +7,7 @@ defmodule ElixirLS.LanguageServer.Providers.CodeLens.TestTest do @project_dir "/project" setup context do - ElixirLS.LanguageServer.Build.load_all_modules() + ElixirLS.LanguageServer.Build.load_all_mix_applications() unless context[:skip_server] do server = ElixirLS.LanguageServer.Test.ServerTestHelpers.start_server() diff --git a/apps/language_server/test/providers/completion_test.exs b/apps/language_server/test/providers/completion_test.exs index f60b923de..8ac722dc2 100644 --- a/apps/language_server/test/providers/completion_test.exs +++ b/apps/language_server/test/providers/completion_test.exs @@ -20,7 +20,7 @@ defmodule ElixirLS.LanguageServer.Providers.CompletionTest do } setup context do - ElixirLS.LanguageServer.Build.load_all_modules() + ElixirLS.LanguageServer.Build.load_all_mix_applications() unless context[:skip_server] do server = ElixirLS.LanguageServer.Test.ServerTestHelpers.start_server() From 27443d4c8526fdb67f9f66b388fa92a4e727b144 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Thu, 2 Jun 2022 12:52:45 +0200 Subject: [PATCH 024/125] make function private --- apps/language_server/lib/language_server/build.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/language_server/lib/language_server/build.ex b/apps/language_server/lib/language_server/build.ex index de57c75d8..23378afe7 100644 --- a/apps/language_server/lib/language_server/build.ex +++ b/apps/language_server/lib/language_server/build.ex @@ -112,7 +112,7 @@ defmodule ElixirLS.LanguageServer.Build do # it was added in https://github.com/elixir-lsp/elixir-ls/pull/227 # removing it doesn't break tests and I'm not able to reproduce # https://github.com/elixir-lsp/elixir-ls/issues/209 on recent elixir (1.13) - def load_all_mix_applications do + defp load_all_mix_applications do apps = cond do Mix.Project.umbrella?() -> From 37a15ed62ce7eaaffe3efeece70cdc631fe7c016 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Samson?= Date: Mon, 6 Jun 2022 06:52:56 +0200 Subject: [PATCH 025/125] update changelog (#697) --- CHANGELOG.md | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 98de14dff..19ad20a28 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,53 @@ ### Unreleased +### v0.10.0: X June 2022 + +Improvements to debugger addapter: + +- A lot of new features around breakpoints: function breakpoints, conditional breakpoints, hit count and log points [#656](https://github.com/elixir-lsp/elixir-ls/pull/656), [#661](https://github.com/elixir-lsp/elixir-ls/pull/661), [#671](https://github.com/elixir-lsp/elixir-ls/pull/671) (thanks [Łukasz Samson](https://github.com/lukaszsamson)) +- Completions in debugger eval console [#679](https://github.com/elixir-lsp/elixir-ls/pull/679) (thanks [Łukasz Samson](https://github.com/lukaszsamson)) +- Debugger evaluate results can now be expanded [#672](https://github.com/elixir-lsp/elixir-ls/pull/672) (thanks [Łukasz Samson](https://github.com/lukaszsamson)) +- Messages in the queue of debugged process can now be examined [#681](https://github.com/elixir-lsp/elixir-ls/pull/681) (thanks [Łukasz Samson](https://github.com/lukaszsamson)) +- Debugger can now handle pause and terminateThread requests [#675](https://github.com/elixir-lsp/elixir-ls/pull/675) (thanks [Łukasz Samson](https://github.com/lukaszsamson)) +- Clipboard and hover eval is now supported in debugger [#680](https://github.com/elixir-lsp/elixir-ls/pull/680) (thanks [Łukasz Samson](https://github.com/lukaszsamson)) +- Auto interpretting can now be disabled [#616](https://github.com/elixir-lsp/elixir-ls/pull/616) (thanks [Jason Axelson](https://github.com/axelson)) +- Debugger conforms better to DAP 1.51 specification [#678](https://github.com/elixir-lsp/elixir-ls/pull/678) (thanks [Łukasz Samson](https://github.com/lukaszsamson)) + +Improvements to language server: + +- Language server can now be restarted via custom command (e.g. from VSCode) [#653](https://github.com/elixir-lsp/elixir-ls/pull/653) (thanks [Łukasz Samson](https://github.com/lukaszsamson)) +- Hover provider adds links to hexdocs.pm [#574](https://github.com/elixir-lsp/elixir-ls/pull/574) (thanks [Fenix](https://github.com/zhenfeng-zhu)) +- Numerous cases of invalid UTF8-UTF16 position conversions fixed [#677](https://github.com/elixir-lsp/elixir-ls/pull/677) (thanks [Łukasz Samson](https://github.com/lukaszsamson)) +- Improved markdown wrapping [#663](https://github.com/elixir-lsp/elixir-ls/pull/663) (thanks [我没有抓狂](https://github.com/BlindingDark)) +- Improved MIX_TARGET environment variable handling [#670](https://github.com/elixir-lsp/elixir-ls/pull/670) (thanks [Masatoshi Nishiguchi](https://github.com/mnishiguchi)) +- defmodule snippet now suggests a module name [#684](https://github.com/elixir-lsp/elixir-ls/pull/684) (thanks [Manos Emmanouilidis](https://github.com/bottlenecked)) +- Constant recompilation on Nerves projects fixed [#686](https://github.com/elixir-lsp/elixir-ls/issues/686) (thanks [Łukasz Samson](https://github.com/lukaszsamson)) +- Invalid negative positions in diagnostics are no longer emitted [#695](https://github.com/elixir-lsp/elixir-ls/pull/695) (thanks [Łukasz Samson](https://github.com/lukaszsamson)) +- Improvements to document symbols provider (https://github.com/elixir-lsp/elixir-ls/commit/1e38db4c9dd9277dfffd9563286f652e3d617a5f) (thanks [Łukasz Samson](https://github.com/lukaszsamson)) +- Added support for OTP 25 new dialyzer options (https://github.com/elixir-lsp/elixir-ls/commit/0da7623f644f79559699e9f002820ad9219d108d) (thanks [Łukasz Samson](https://github.com/lukaszsamson)) +- Improvements to complete (operator, sigil, bitstring) [#150](https://github.com/elixir-lsp/elixir_sense/pull/150), (https://github.com/elixir-lsp/elixir_sense/commit/33df514a1254455f54cb069999454c7e8586eb2d) (thanks [Łukasz Samson](https://github.com/lukaszsamson)) +- Improved alias resolution (https://github.com/elixir-lsp/elixir_sense/issues/151) (thanks [Łukasz Samson](https://github.com/lukaszsamson)) +- Fixed crash on OTP 24.2 (https://github.com/elixir-lsp/elixir_sense/commit/72f3d4ffee3c11c289d47d14a6c5f6e1a4afacb4) (thanks [Łukasz Samson](https://github.com/lukaszsamson)) +- Better function detection when hovering inside string interpolation [#152](https://github.com/elixir-lsp/elixir_sense/pull/152) (thanks [Milo Lee](https://github.com/oo6)) +- Support for external plugins to elixir_sense [#141](https://github.com/elixir-lsp/elixir_sense/pull/141) (thanks [Zach Daniel](https://github.com/zachdaniel)) + +VSCode: + +- To Pipe and From Pipe code transformation command [#182](https://github.com/elixir-lsp/vscode-elixir-ls/pull/182) (thanks [Paulo Valente](https://github.com/polvalente)) +- Restart language server command added [#218](https://github.com/elixir-lsp/vscode-elixir-ls/pull/218) (thanks [Łukasz Samson](https://github.com/lukaszsamson)) +- New settings related to auto interpreting in debugger (https://github.com/elixir-lsp/vscode-elixir-ls/commit/4294f9f0da6819e519aa4278f5f2d553ff054dac) (thanks [Jason Axelson](https://github.com/axelson)) +- New OTP 25 dialyzer settings (https://github.com/elixir-lsp/vscode-elixir-ls/commit/50a8a53fa79c14d2ea4031f872ec3d7cd32155f5) (thanks [Łukasz Samson](https://github.com/lukaszsamson)) +- Compile time environment variables can now be set in extension config [#213](https://github.com/elixir-lsp/vscode-elixir-ls/pull/213) (thanks [vacarsu](https://github.com/vacarsu)) +- Additional watched extensions can now be set in extension config [#197](https://github.com/elixir-lsp/vscode-elixir-ls/pull/197) (thanks [Vanja Bucic](https://github.com/vanjabucic)) +- Improved unquite_slicing highlighting [#221](https://github.com/elixir-lsp/vscode-elixir-ls/pull/221) (thanks [Milo Lee](https://github.com/oo6)) +- Improved string interpolation highlighting [#229](https://github.com/elixir-lsp/vscode-elixir-ls/pull/229) (thanks [Milo Lee](https://github.com/oo6)) +- Improved regex with < highlighting [#226](https://github.com/elixir-lsp/vscode-elixir-ls/pull/226) (thanks [Tiago Moraes](https://github.com/tiagoefmoraes)) +- Extension updated to use LSP v3.16 [#227](https://github.com/elixir-lsp/vscode-elixir-ls/pull/227) (thanks [Łukasz Samson](https://github.com/lukaszsamson)) + +Houskeeping: + +thanks [Łukasz Samson](https://github.com/lukaszsamson), [Thanabodee Charoenpiriyakij](https://github.com/wingyplus), [Daniils Petrovs](https://github.com/DaniruKun), [Jason Axelson](https://github.com/axelson) + ### v0.9.0: 4 December 2021 Improvements: From e3278b82d4efc8bfe19d6ebe70cfa6b5d0c6596d Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Mon, 6 Jun 2022 06:35:26 +0200 Subject: [PATCH 026/125] wip --- .../providers/document_symbols.ex | 97 +++++++++++++------ .../test/providers/document_symbols_test.exs | 2 +- 2 files changed, 66 insertions(+), 33 deletions(-) diff --git a/apps/language_server/lib/language_server/providers/document_symbols.ex b/apps/language_server/lib/language_server/providers/document_symbols.ex index afc3f0ab4..7299a85a5 100644 --- a/apps/language_server/lib/language_server/providers/document_symbols.ex +++ b/apps/language_server/lib/language_server/providers/document_symbols.ex @@ -10,7 +10,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbols do require ElixirLS.LanguageServer.Protocol, as: Protocol defmodule Info do - defstruct [:type, :name, :location, :children] + defstruct [:type, :name, :location, :children, :selection_location] end @defs [:def, :defp, :defmacro, :defmacrop, :defguard, :defguardp, :defdelegate] @@ -49,7 +49,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbols do end defp list_symbols(src) do - case ElixirSense.string_to_quoted(src, 1, @max_parser_errors, line: 1) do + case ElixirSense.string_to_quoted(src, 1, @max_parser_errors, line: 1, token_metadata: true) do {:ok, quoted_form} -> {:ok, extract_modules(quoted_form)} {:error, _error} -> {:error, :compilation_error} end @@ -74,18 +74,22 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbols do # Modules, protocols defp extract_symbol(_module_name, {defname, location, arguments}) when defname in [:defmodule, :defprotocol] do - {module_name, module_body} = + {module_name, module_name_location, module_body} = case arguments do # Handles `defmodule do\nend` type compile errors [[do: module_body]] -> # The LSP requires us to return a non-empty name case defname do - :defmodule -> {"MISSING_MODULE_NAME", module_body} - :defprotocol -> {"MISSING_PROTOCOL_NAME", module_body} + :defmodule -> {"MISSING_MODULE_NAME", nil, module_body} + :defprotocol -> {"MISSING_PROTOCOL_NAME", nil, module_body} end [module_expression, [do: module_body]] -> - {extract_module_name(module_expression), module_body} + module_name_location = case module_expression do + {_, location, _} -> location + _ -> nil + end + {extract_module_name(module_expression), module_name_location, module_body} end mod_defns = @@ -105,7 +109,13 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbols do :defprotocol -> :interface end - %Info{type: type, name: module_name, location: location, children: module_symbols} + %Info{ + type: type, + name: module_name, + location: location, + selection_location: module_name_location, + children: module_symbols + } end # Protocol implementations @@ -132,6 +142,8 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbols do [] end + # TODO there is no end/closing metadata in the AST + %Info{ type: :struct, name: "#{defname} #{module_name}", @@ -144,7 +156,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbols do defp extract_symbol(_, {:@, _, [{kind, _, _}]}) when kind in @supplementing_attributes, do: nil # Types - defp extract_symbol(_current_module, {:@, _, [{type_kind, location, type_expression}]}) + defp extract_symbol(_current_module, {:@, location, [{type_kind, type_head_location, type_expression}]}) when type_kind in [:type, :typep, :opaque, :callback, :macrocallback] do type_name = case type_expression do @@ -162,26 +174,27 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbols do type: type, name: "@#{type_kind} #{type_name}", location: location, + selection_location: type_head_location, children: [] } end # @behaviour BehaviourModule - defp extract_symbol(_current_module, {:@, _, [{:behaviour, location, [behaviour_expression]}]}) do + defp extract_symbol(_current_module, {:@, location, [{:behaviour, _, [behaviour_expression]}]}) do module_name = extract_module_name(behaviour_expression) %Info{type: :interface, name: "@behaviour #{module_name}", location: location, children: []} end # Other attributes - defp extract_symbol(_current_module, {:@, _, [{name, location, _}]}) do + defp extract_symbol(_current_module, {:@, location, [{name, _, _}]}) do %Info{type: :constant, name: "@#{name}", location: location, children: []} end # Function, macro, guard with when defp extract_symbol( _current_module, - {defname, _, [{:when, _, [{_, location, _} = fn_head, _]} | _]} + {defname, location, [{:when, _, [{_, head_location, _} = fn_head, _]} | _]} ) when defname in @defs do name = Macro.to_string(fn_head) |> String.replace("\n", "") @@ -190,12 +203,13 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbols do type: :function, name: "#{defname} #{name}", location: location, + selection_location: head_location, children: [] } end # Function, macro, delegate - defp extract_symbol(_current_module, {defname, _, [{_, location, _} = fn_head | _]}) + defp extract_symbol(_current_module, {defname, location, [{_, head_location, _} = fn_head | _]}) when defname in @defs do name = Macro.to_string(fn_head) |> String.replace("\n", "") @@ -203,21 +217,31 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbols do type: :function, name: "#{defname} #{name}", location: location, + selection_location: head_location, children: [] } end defp extract_symbol( _current_module, - {{:., location, [{:__aliases__, _, [:Record]}, :defrecord]}, _, [record_name, _]} + {{:., _, [{:__aliases__, alias_location, [:Record]}, :defrecord]}, location, [record_name, properties]} ) do name = Macro.to_string(record_name) |> String.replace("\n", "") + children = + if is_list(properties) do + properties + |> Enum.map(&extract_property(&1, location)) + |> Enum.reject(&is_nil/1) + else + [] + end + %Info{ type: :class, name: "defrecord #{name}", - location: location, - children: [] + location: location |> Keyword.merge(Keyword.take(alias_location, [:line, :column])), + children: children } end @@ -269,21 +293,20 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbols do keys = case config_entry do list when is_list(list) -> - list - |> Enum.map(fn {key, _} -> Macro.to_string(key) end) + string_list = list + |> Enum.map_join(", ", fn {key, _} -> Macro.to_string(key) end) + "[#{string_list}]" key -> - [Macro.to_string(key)] + Macro.to_string(key) end - for key <- keys do - %Info{ - type: :key, - name: "config :#{app} #{key}", - location: location, - children: [] - } - end + %Info{ + type: :key, + name: "config :#{app} #{keys}", + location: location, + children: [] + } end defp extract_symbol(_, _), do: nil @@ -292,13 +315,11 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbols do do: Enum.map(info, &build_symbol_information_hierarchical(uri, text, &1)) defp build_symbol_information_hierarchical(uri, text, %Info{} = info) do - range = location_to_range(info.location, text) - %Protocol.DocumentSymbol{ name: info.name, kind: SymbolUtils.symbol_kind_to_code(info.type), - range: range, - selectionRange: range, + range: location_to_range(info.location, text), + selectionRange: location_to_range(info.selection_location || info.location, text), children: build_symbol_information_hierarchical(uri, text, info.children) } end @@ -338,10 +359,22 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbols do end defp location_to_range(location, text) do - {line, character} = + {start_line, start_character} = SourceFile.elixir_position_to_lsp(text, {location[:line], location[:column]}) - Protocol.range(line, character, line, character) + {end_line, end_character} = cond do + end_location = location[:end_of_expression] -> + SourceFile.elixir_position_to_lsp(text, {end_location[:line], end_location[:column]}) + end_location = location[:end] -> + SourceFile.elixir_position_to_lsp(text, {end_location[:line], end_location[:column] + 3}) + end_location = location[:closing] -> + # all closing tags we expect hera are 1 char width + SourceFile.elixir_position_to_lsp(text, {end_location[:line], end_location[:column] + 1}) + true -> + {start_line, start_character} + end + + Protocol.range(start_line, start_character, end_line, end_character) end defp extract_module_name(protocol: protocol, implementations: implementations) do diff --git a/apps/language_server/test/providers/document_symbols_test.exs b/apps/language_server/test/providers/document_symbols_test.exs index 293c9bb3a..6cfd61f9a 100644 --- a/apps/language_server/test/providers/document_symbols_test.exs +++ b/apps/language_server/test/providers/document_symbols_test.exs @@ -3032,7 +3032,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do name: "MISSING_PROTOCOL_NAME", range: %{ "start" => %{"line" => 0, "character" => 0}, - "end" => %{"line" => 0, "character" => 0} + "end" => %{"line" => 1, "character" => 3} }, selectionRange: %{ "start" => %{"line" => 0, "character" => 0}, From 539ba8814f26916c031d12d612c5550e31498ad5 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Fri, 10 Jun 2022 07:35:21 +0200 Subject: [PATCH 027/125] update changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 19ad20a28..fedd9a36d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ ### Unreleased -### v0.10.0: X June 2022 +### v0.10.0: 10 June 2022 Improvements to debugger addapter: From 60e2df298ee6f105b5d15b0565f7c9b1f808c376 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sat, 11 Jun 2022 11:02:07 +0200 Subject: [PATCH 028/125] run formatter --- .../lib/language_server/build.ex | 2 +- .../lib/language_server/diagnostics.ex | 2 +- .../lib/language_server/dialyzer/analyzer.ex | 33 +++++++++---------- 3 files changed, 18 insertions(+), 19 deletions(-) diff --git a/apps/language_server/lib/language_server/build.ex b/apps/language_server/lib/language_server/build.ex index 23378afe7..8afccfc70 100644 --- a/apps/language_server/lib/language_server/build.ex +++ b/apps/language_server/lib/language_server/build.ex @@ -55,7 +55,7 @@ defmodule ElixirLS.LanguageServer.Build do end defp reload_project do - mixfile = Path.absname(MixfileHelpers.mix_exs) + mixfile = Path.absname(MixfileHelpers.mix_exs()) if File.exists?(mixfile) do # FIXME: Private API diff --git a/apps/language_server/lib/language_server/diagnostics.ex b/apps/language_server/lib/language_server/diagnostics.ex index 2ec5f8d2a..9b8867d2e 100644 --- a/apps/language_server/lib/language_server/diagnostics.ex +++ b/apps/language_server/lib/language_server/diagnostics.ex @@ -238,7 +238,7 @@ defmodule ElixirLS.LanguageServer.Diagnostics do %Mix.Task.Compiler.Diagnostic{ compiler_name: "ElixirLS", - file: Path.absname(MixfileHelpers.mix_exs), + file: Path.absname(MixfileHelpers.mix_exs()), # 0 means unknown position: 0, message: msg, diff --git a/apps/language_server/lib/language_server/dialyzer/analyzer.ex b/apps/language_server/lib/language_server/dialyzer/analyzer.ex index c3e708ed7..8641c6d0c 100644 --- a/apps/language_server/lib/language_server/dialyzer/analyzer.ex +++ b/apps/language_server/lib/language_server/dialyzer/analyzer.ex @@ -28,23 +28,22 @@ defmodule ElixirLS.LanguageServer.Dialyzer.Analyzer do :warn_undefined_callbacks ] @non_default_warns [ - :warn_contract_not_equal, - :warn_contract_subtype, - :warn_contract_supertype, - :warn_return_only_exit, - :warn_umatched_return, - :warn_unknown - ] ++ ( - if String.to_integer(System.otp_release()) >= 25 do - [ - # OTP >= 25 options - :warn_contract_missing_return, - :warn_contract_extra_return - ] - else - [] - end - ) + :warn_contract_not_equal, + :warn_contract_subtype, + :warn_contract_supertype, + :warn_return_only_exit, + :warn_umatched_return, + :warn_unknown + ] ++ + (if String.to_integer(System.otp_release()) >= 25 do + [ + # OTP >= 25 options + :warn_contract_missing_return, + :warn_contract_extra_return + ] + else + [] + end) @log_cache_length 10 defstruct [ From d59b939c41614ccfedee1a54143824aceacbcddb Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sat, 11 Jun 2022 11:04:52 +0200 Subject: [PATCH 029/125] When possible return non empty ranges and selection rages in documentSymbol provider Fixes https://github.com/elixir-lsp/elixir-ls/issues/700 --- .../providers/document_symbols.ex | 69 +- .../test/providers/document_symbols_test.exs | 1202 +++++------------ 2 files changed, 350 insertions(+), 921 deletions(-) diff --git a/apps/language_server/lib/language_server/providers/document_symbols.ex b/apps/language_server/lib/language_server/providers/document_symbols.ex index 7299a85a5..072835600 100644 --- a/apps/language_server/lib/language_server/providers/document_symbols.ex +++ b/apps/language_server/lib/language_server/providers/document_symbols.ex @@ -85,10 +85,13 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbols do end [module_expression, [do: module_body]] -> - module_name_location = case module_expression do - {_, location, _} -> location - _ -> nil - end + module_name_location = + case module_expression do + {_, location, _} -> location + _ -> nil + end + + # TODO extract module name location from Code.Fragment.surround_context? {extract_module_name(module_expression), module_name_location, module_body} end @@ -156,20 +159,23 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbols do defp extract_symbol(_, {:@, _, [{kind, _, _}]}) when kind in @supplementing_attributes, do: nil # Types - defp extract_symbol(_current_module, {:@, location, [{type_kind, type_head_location, type_expression}]}) + defp extract_symbol(_current_module, {:@, location, [{type_kind, _, type_expression}]}) when type_kind in [:type, :typep, :opaque, :callback, :macrocallback] do - type_name = + {type_name, type_head_location} = case type_expression do - [{:"::", _, [{_, _, _} = type_head | _]}] -> - Macro.to_string(type_head) + [{:"::", _, [{_, type_head_location, _} = type_head | _]}] -> + {Macro.to_string(type_head), type_head_location} - [{:when, _, [{:"::", _, [{_, _, _} = type_head, _]}, _]}] -> - Macro.to_string(type_head) + [{:when, _, [{:"::", _, [{_, type_head_location, _} = type_head, _]}, _]}] -> + {Macro.to_string(type_head), type_head_location} end + + type_name = + type_name |> String.replace("\n", "") type = if type_kind in [:type, :typep, :opaque], do: :class, else: :event - + # TODO no closing metdata in type expressions %Info{ type: type, name: "@#{type_kind} #{type_name}", @@ -224,7 +230,8 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbols do defp extract_symbol( _current_module, - {{:., _, [{:__aliases__, alias_location, [:Record]}, :defrecord]}, location, [record_name, properties]} + {{:., _, [{:__aliases__, alias_location, [:Record]}, :defrecord]}, location, + [record_name, properties]} ) do name = Macro.to_string(record_name) |> String.replace("\n", "") @@ -293,8 +300,10 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbols do keys = case config_entry do list when is_list(list) -> - string_list = list - |> Enum.map_join(", ", fn {key, _} -> Macro.to_string(key) end) + string_list = + list + |> Enum.map_join(", ", fn {key, _} -> Macro.to_string(key) end) + "[#{string_list}]" key -> @@ -362,17 +371,27 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbols do {start_line, start_character} = SourceFile.elixir_position_to_lsp(text, {location[:line], location[:column]}) - {end_line, end_character} = cond do - end_location = location[:end_of_expression] -> - SourceFile.elixir_position_to_lsp(text, {end_location[:line], end_location[:column]}) - end_location = location[:end] -> - SourceFile.elixir_position_to_lsp(text, {end_location[:line], end_location[:column] + 3}) - end_location = location[:closing] -> - # all closing tags we expect hera are 1 char width - SourceFile.elixir_position_to_lsp(text, {end_location[:line], end_location[:column] + 1}) - true -> - {start_line, start_character} - end + {end_line, end_character} = + cond do + end_location = location[:end_of_expression] -> + SourceFile.elixir_position_to_lsp(text, {end_location[:line], end_location[:column]}) + + end_location = location[:end] -> + SourceFile.elixir_position_to_lsp( + text, + {end_location[:line], end_location[:column] + 3} + ) + + end_location = location[:closing] -> + # all closing tags we expect hera are 1 char width + SourceFile.elixir_position_to_lsp( + text, + {end_location[:line], end_location[:column] + 1} + ) + + true -> + {start_line, start_character} + end Protocol.range(start_line, start_character, end_line, end_character) end diff --git a/apps/language_server/test/providers/document_symbols_test.exs b/apps/language_server/test/providers/document_symbols_test.exs index 6cfd61f9a..22061f119 100644 --- a/apps/language_server/test/providers/document_symbols_test.exs +++ b/apps/language_server/test/providers/document_symbols_test.exs @@ -30,6 +30,16 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do after :ok end + def fun_multiple_when(term \\ nil) + def fun_multiple_when(term) + when is_integer(term) + when is_float(term) + when is_nil(term) do + :maybe_number + end + def fun_multiple_when(_other) do + :something_else + end end ] @@ -42,12 +52,12 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do kind: 14, name: "@my_mod_var", range: %{ - "end" => %{"character" => 9, "line" => 2}, - "start" => %{"character" => 9, "line" => 2} + "end" => %{"character" => 37, "line" => 2}, + "start" => %{"character" => 8, "line" => 2} }, selectionRange: %{ - "end" => %{"character" => 9, "line" => 2}, - "start" => %{"character" => 9, "line" => 2} + "end" => %{"character" => 37, "line" => 2}, + "start" => %{"character" => 8, "line" => 2} } }, %Protocol.DocumentSymbol{ @@ -55,154 +65,145 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do kind: 12, name: "def my_fn(arg)", range: %{ - "end" => %{"character" => 12, "line" => 3}, - "start" => %{"character" => 12, "line" => 3} + "end" => %{"character" => 31, "line" => 3}, + "start" => %{"character" => 8, "line" => 3} }, selectionRange: %{ - "end" => %{"character" => 12, "line" => 3}, + "end" => %{"character" => 22, "line" => 3}, "start" => %{"character" => 12, "line" => 3} } }, %Protocol.DocumentSymbol{ children: [], kind: 12, - name: "defp my_private_fn(arg)", - range: %{ - "end" => %{"character" => 13, "line" => 4}, - "start" => %{"character" => 13, "line" => 4} - }, - selectionRange: %{ - "end" => %{"character" => 13, "line" => 4}, - "start" => %{"character" => 13, "line" => 4} - } + name: "defp my_private_fn(arg)" }, %Protocol.DocumentSymbol{ children: [], kind: 12, - name: "defmacro my_macro()", - range: %{ - "end" => %{"character" => 17, "line" => 5}, - "start" => %{"character" => 17, "line" => 5} - }, - selectionRange: %{ - "end" => %{"character" => 17, "line" => 5}, - "start" => %{"character" => 17, "line" => 5} - } + name: "defmacro my_macro()" }, %Protocol.DocumentSymbol{ children: [], kind: 12, - name: "defmacrop my_private_macro()", - range: %{ - "end" => %{"character" => 18, "line" => 6}, - "start" => %{"character" => 18, "line" => 6} - }, - selectionRange: %{ - "end" => %{"character" => 18, "line" => 6}, - "start" => %{"character" => 18, "line" => 6} - } + name: "defmacrop my_private_macro()" }, %Protocol.DocumentSymbol{ children: [], kind: 12, name: "defguard my_guard(a)", range: %{ - "end" => %{"character" => 17, "line" => 7}, - "start" => %{"character" => 17, "line" => 7} + "end" => %{"character" => 47, "line" => 7}, + "start" => %{"character" => 8, "line" => 7} }, selectionRange: %{ - "end" => %{"character" => 17, "line" => 7}, + "end" => %{"character" => 28, "line" => 7}, "start" => %{"character" => 17, "line" => 7} } }, %Protocol.DocumentSymbol{ children: [], kind: 12, - name: "defguardp my_private_guard(a)", + name: "defguardp my_private_guard(a)" + }, + %Protocol.DocumentSymbol{ + children: [], + kind: 12, + name: "defdelegate my_delegate(list)", range: %{ - "end" => %{"character" => 18, "line" => 8}, - "start" => %{"character" => 18, "line" => 8} + "end" => %{"character" => 61, "line" => 9}, + "start" => %{"character" => 8, "line" => 9} }, selectionRange: %{ - "end" => %{"character" => 18, "line" => 8}, - "start" => %{"character" => 18, "line" => 8} + "end" => %{"character" => 37, "line" => 9}, + "start" => %{"character" => 20, "line" => 9} } }, %Protocol.DocumentSymbol{ children: [], kind: 12, - name: "defdelegate my_delegate(list)", + name: "defguard my_guard" + }, + %Protocol.DocumentSymbol{ + children: [], + kind: 12, + name: "def my_fn_no_arg", range: %{ - "end" => %{"character" => 20, "line" => 9}, - "start" => %{"character" => 20, "line" => 9} + "end" => %{"character" => 33, "line" => 11}, + "start" => %{"character" => 8, "line" => 11} }, selectionRange: %{ - "end" => %{"character" => 20, "line" => 9}, - "start" => %{"character" => 20, "line" => 9} + "end" => %{"character" => 12, "line" => 11}, + "start" => %{"character" => 12, "line" => 11} } }, %Protocol.DocumentSymbol{ children: [], kind: 12, - name: "defguard my_guard", + name: "def my_fn_with_guard(arg)" + }, + %Protocol.DocumentSymbol{ + children: [], + kind: 12, + name: "def my_fn_with_more_blocks(arg)", range: %{ - "end" => %{"character" => 17, "line" => 10}, - "start" => %{"character" => 17, "line" => 10} + "end" => %{"character" => 11, "line" => 23}, + "start" => %{"character" => 8, "line" => 13} }, selectionRange: %{ - "end" => %{"character" => 17, "line" => 10}, - "start" => %{"character" => 17, "line" => 10} + "end" => %{"character" => 39, "line" => 13}, + "start" => %{"character" => 12, "line" => 13} } }, %Protocol.DocumentSymbol{ children: [], kind: 12, - name: "def my_fn_no_arg", + name: "def fun_multiple_when(term \\\\ nil)", range: %{ - "end" => %{"character" => 12, "line" => 11}, - "start" => %{"character" => 12, "line" => 11} + "end" => %{"character" => 42, "line" => 24}, + "start" => %{"character" => 8, "line" => 24} }, selectionRange: %{ - "end" => %{"character" => 12, "line" => 11}, - "start" => %{"character" => 12, "line" => 11} + "end" => %{"character" => 42, "line" => 24}, + "start" => %{"character" => 12, "line" => 24} } }, %Protocol.DocumentSymbol{ children: [], kind: 12, - name: "def my_fn_with_guard(arg)", + name: "def fun_multiple_when(term)", range: %{ - "end" => %{"character" => 12, "line" => 12}, - "start" => %{"character" => 12, "line" => 12} + "end" => %{"character" => 11, "line" => 30}, + "start" => %{"character" => 8, "line" => 25} }, selectionRange: %{ - "end" => %{"character" => 12, "line" => 12}, - "start" => %{"character" => 12, "line" => 12} + "end" => %{"character" => 35, "line" => 25}, + "start" => %{"character" => 12, "line" => 25} } }, %Protocol.DocumentSymbol{ children: [], kind: 12, - name: "def my_fn_with_more_blocks(arg)", + name: "def fun_multiple_when(_other)", range: %{ - "end" => %{"character" => 12, "line" => 13}, - "start" => %{"character" => 12, "line" => 13} + "end" => %{"character" => 11, "line" => 33}, + "start" => %{"character" => 8, "line" => 31} }, selectionRange: %{ - "end" => %{"character" => 12, "line" => 13}, - "start" => %{"character" => 12, "line" => 13} + "end" => %{"character" => 37, "line" => 31}, + "start" => %{"character" => 12, "line" => 31} } } ], kind: 2, name: "MyModule", range: %{ - "end" => %{"character" => 6, "line" => 1}, + "end" => %{"character" => 9, "line" => 34}, "start" => %{"character" => 6, "line" => 1} }, selectionRange: %{ - "end" => %{"character" => 6, "line" => 1}, - "start" => %{"character" => 6, "line" => 1} + "end" => %{"character" => 16, "line" => 1}, + "start" => %{"character" => 16, "line" => 1} } } ]} = DocumentSymbols.symbols(uri, text, true) @@ -244,7 +245,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do kind: 2, location: %{ range: %{ - "end" => %{"character" => 6, "line" => 1}, + "end" => %{"character" => 9, "line" => 24}, "start" => %{"character" => 6, "line" => 1} } } @@ -254,8 +255,8 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do kind: 14, location: %{ range: %{ - "end" => %{"character" => 9, "line" => 2}, - "start" => %{"character" => 9, "line" => 2} + "end" => %{"character" => 37, "line" => 2}, + "start" => %{"character" => 8, "line" => 2} } }, containerName: "MyModule" @@ -265,8 +266,8 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do kind: 12, location: %{ range: %{ - "end" => %{"character" => 12, "line" => 3}, - "start" => %{"character" => 12, "line" => 3} + "end" => %{"character" => 31, "line" => 3}, + "start" => %{"character" => 8, "line" => 3} } }, containerName: "MyModule" @@ -274,34 +275,16 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do %Protocol.SymbolInformation{ name: "defp my_private_fn(arg)", kind: 12, - location: %{ - range: %{ - "end" => %{"character" => 13, "line" => 4}, - "start" => %{"character" => 13, "line" => 4} - } - }, containerName: "MyModule" }, %Protocol.SymbolInformation{ name: "defmacro my_macro()", kind: 12, - location: %{ - range: %{ - "end" => %{"character" => 17, "line" => 5}, - "start" => %{"character" => 17, "line" => 5} - } - }, containerName: "MyModule" }, %Protocol.SymbolInformation{ name: "defmacrop my_private_macro()", kind: 12, - location: %{ - range: %{ - "end" => %{"character" => 18, "line" => 6}, - "start" => %{"character" => 18, "line" => 6} - } - }, containerName: "MyModule" }, %Protocol.SymbolInformation{ @@ -309,8 +292,8 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do kind: 12, location: %{ range: %{ - "end" => %{"character" => 17, "line" => 7}, - "start" => %{"character" => 17, "line" => 7} + "end" => %{"character" => 47, "line" => 7}, + "start" => %{"character" => 8, "line" => 7} } }, containerName: "MyModule" @@ -318,12 +301,6 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do %Protocol.SymbolInformation{ name: "defguardp my_private_guard(a)", kind: 12, - location: %{ - range: %{ - "end" => %{"character" => 18, "line" => 8}, - "start" => %{"character" => 18, "line" => 8} - } - }, containerName: "MyModule" }, %Protocol.SymbolInformation{ @@ -331,8 +308,8 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do kind: 12, location: %{ range: %{ - "end" => %{"character" => 20, "line" => 9}, - "start" => %{"character" => 20, "line" => 9} + "end" => %{"character" => 61, "line" => 9}, + "start" => %{"character" => 8, "line" => 9} } }, containerName: "MyModule" @@ -340,34 +317,16 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do %Protocol.SymbolInformation{ name: "defguard my_guard", kind: 12, - location: %{ - range: %{ - "end" => %{"character" => 17, "line" => 10}, - "start" => %{"character" => 17, "line" => 10} - } - }, containerName: "MyModule" }, %Protocol.SymbolInformation{ name: "def my_fn_no_arg", kind: 12, - location: %{ - range: %{ - "end" => %{"character" => 12, "line" => 11}, - "start" => %{"character" => 12, "line" => 11} - } - }, containerName: "MyModule" }, %Protocol.SymbolInformation{ name: "def my_fn_with_guard(arg)", kind: 12, - location: %{ - range: %{ - "end" => %{"character" => 12, "line" => 12}, - "start" => %{"character" => 12, "line" => 12} - } - }, containerName: "MyModule" }, %Protocol.SymbolInformation{ @@ -375,8 +334,8 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do kind: 12, location: %{ range: %{ - "end" => %{"character" => 12, "line" => 13}, - "start" => %{"character" => 12, "line" => 13} + "end" => %{"character" => 11, "line" => 23}, + "start" => %{"character" => 8, "line" => 13} } }, containerName: "MyModule" @@ -403,38 +362,30 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do %Protocol.DocumentSymbol{ children: [], kind: 12, - name: "def my_fn()", - range: %{ - "end" => %{"character" => 14, "line" => 3}, - "start" => %{"character" => 14, "line" => 3} - }, - selectionRange: %{ - "end" => %{"character" => 14, "line" => 3}, - "start" => %{"character" => 14, "line" => 3} - } + name: "def my_fn()" } ], kind: 2, name: "SubModule", range: %{ - "end" => %{"character" => 8, "line" => 2}, + "end" => %{"character" => 11, "line" => 4}, "start" => %{"character" => 8, "line" => 2} }, selectionRange: %{ - "end" => %{"character" => 8, "line" => 2}, - "start" => %{"character" => 8, "line" => 2} + "end" => %{"character" => 18, "line" => 2}, + "start" => %{"character" => 18, "line" => 2} } } ], kind: 2, name: "MyModule", range: %{ - "end" => %{"character" => 6, "line" => 1}, + "end" => %{"character" => 9, "line" => 5}, "start" => %{"character" => 6, "line" => 1} }, selectionRange: %{ - "end" => %{"character" => 6, "line" => 1}, - "start" => %{"character" => 6, "line" => 1} + "end" => %{"character" => 16, "line" => 1}, + "start" => %{"character" => 16, "line" => 1} } } ]} = DocumentSymbols.symbols(uri, text, true) @@ -457,7 +408,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do kind: 2, location: %{ range: %{ - "end" => %{"character" => 6, "line" => 1}, + "end" => %{"character" => 9, "line" => 5}, "start" => %{"character" => 6, "line" => 1} } } @@ -467,7 +418,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do kind: 2, location: %{ range: %{ - "end" => %{"character" => 8, "line" => 2}, + "end" => %{"character" => 11, "line" => 4}, "start" => %{"character" => 8, "line" => 2} } }, @@ -476,12 +427,6 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do %Protocol.SymbolInformation{ kind: 12, name: "def my_fn()", - location: %{ - range: %{ - "end" => %{"character" => 14, "line" => 3}, - "start" => %{"character" => 14, "line" => 3} - } - }, containerName: "SubModule" } ]} = DocumentSymbols.symbols(uri, text, false) @@ -505,26 +450,18 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do %Protocol.DocumentSymbol{ children: [], kind: 12, - name: "def some_function()", - range: %{ - "end" => %{"character" => 12, "line" => 2}, - "start" => %{"character" => 12, "line" => 2} - }, - selectionRange: %{ - "end" => %{"character" => 12, "line" => 2}, - "start" => %{"character" => 12, "line" => 2} - } + name: "def some_function()" } ], kind: 2, name: "MyModule", range: %{ - "end" => %{"character" => 6, "line" => 1}, + "end" => %{"character" => 9, "line" => 3}, "start" => %{"character" => 6, "line" => 1} }, selectionRange: %{ - "end" => %{"character" => 6, "line" => 1}, - "start" => %{"character" => 6, "line" => 1} + "end" => %{"character" => 16, "line" => 1}, + "start" => %{"character" => 16, "line" => 1} } }, %Protocol.DocumentSymbol{ @@ -532,26 +469,18 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do %Protocol.DocumentSymbol{ children: [], kind: 12, - name: "def some_other_function()", - range: %{ - "end" => %{"character" => 12, "line" => 5}, - "start" => %{"character" => 12, "line" => 5} - }, - selectionRange: %{ - "end" => %{"character" => 12, "line" => 5}, - "start" => %{"character" => 12, "line" => 5} - } + name: "def some_other_function()" } ], kind: 2, name: "MyOtherModule", range: %{ - "end" => %{"character" => 6, "line" => 4}, + "end" => %{"character" => 9, "line" => 6}, "start" => %{"character" => 6, "line" => 4} }, selectionRange: %{ - "end" => %{"character" => 6, "line" => 4}, - "start" => %{"character" => 6, "line" => 4} + "end" => %{"character" => 16, "line" => 4}, + "start" => %{"character" => 16, "line" => 4} } } ]} = DocumentSymbols.symbols(uri, text, true) @@ -575,7 +504,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do kind: 2, location: %{ range: %{ - "end" => %{"character" => 6, "line" => 1}, + "end" => %{"character" => 9, "line" => 3}, "start" => %{"character" => 6, "line" => 1} } } @@ -583,12 +512,6 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do %Protocol.SymbolInformation{ name: "def some_function()", kind: 12, - location: %{ - range: %{ - "end" => %{"character" => 12, "line" => 2}, - "start" => %{"character" => 12, "line" => 2} - } - }, containerName: "MyModule" }, %Protocol.SymbolInformation{ @@ -596,7 +519,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do kind: 2, location: %{ range: %{ - "end" => %{"character" => 6, "line" => 4}, + "end" => %{"character" => 9, "line" => 6}, "start" => %{"character" => 6, "line" => 4} } } @@ -604,12 +527,6 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do %Protocol.SymbolInformation{ kind: 12, name: "def some_other_function()", - location: %{ - range: %{ - "end" => %{"character" => 12, "line" => 5}, - "start" => %{"character" => 12, "line" => 5} - } - }, containerName: "MyOtherModule" } ]} = DocumentSymbols.symbols(uri, text, false) @@ -630,25 +547,17 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do %Protocol.DocumentSymbol{ children: [], kind: 12, - name: "def my_fn()", - range: %{ - "end" => %{"character" => 12, "line" => 2}, - "start" => %{"character" => 12, "line" => 2} - }, - selectionRange: %{ - "end" => %{"character" => 12, "line" => 2}, - "start" => %{"character" => 12, "line" => 2} - } + name: "def my_fn()" } ], kind: 2, name: "MyModule", range: %{ - "end" => %{"character" => 6, "line" => 1}, + "end" => %{"character" => 9, "line" => 3}, "start" => %{"character" => 6, "line" => 1} }, selectionRange: %{ - "end" => %{"character" => 6, "line" => 1}, + "end" => %{"character" => 9, "line" => 3}, "start" => %{"character" => 6, "line" => 1} } } @@ -670,7 +579,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do kind: 2, location: %{ range: %{ - "end" => %{"character" => 6, "line" => 1}, + "end" => %{"character" => 9, "line" => 3}, "start" => %{"character" => 6, "line" => 1} } } @@ -678,12 +587,6 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do %Protocol.SymbolInformation{ name: "def my_fn()", kind: 12, - location: %{ - range: %{ - "end" => %{"character" => 12, "line" => 2}, - "start" => %{"character" => 12, "line" => 2} - } - }, containerName: "MyModule" } ]} = DocumentSymbols.symbols(uri, text, false) @@ -704,26 +607,18 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do %Protocol.DocumentSymbol{ children: [], kind: 12, - name: "def my_fn()", - range: %{ - "end" => %{"character" => 12, "line" => 2}, - "start" => %{"character" => 12, "line" => 2} - }, - selectionRange: %{ - "end" => %{"character" => 12, "line" => 2}, - "start" => %{"character" => 12, "line" => 2} - } + name: "def my_fn()" } ], kind: 2, name: "# unknown", range: %{ - "end" => %{"character" => 6, "line" => 1}, + "end" => %{"character" => 9, "line" => 3}, "start" => %{"character" => 6, "line" => 1} }, selectionRange: %{ - "end" => %{"character" => 6, "line" => 1}, - "start" => %{"character" => 6, "line" => 1} + "end" => %{"character" => 28, "line" => 1}, + "start" => %{"character" => 16, "line" => 1} } } ]} = DocumentSymbols.symbols(uri, text, true) @@ -744,7 +639,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do kind: 2, location: %{ range: %{ - "end" => %{"character" => 6, "line" => 1}, + "end" => %{"character" => 9, "line" => 3}, "start" => %{"character" => 6, "line" => 1} } } @@ -752,12 +647,6 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do %Protocol.SymbolInformation{ kind: 12, name: "def my_fn()", - location: %{ - range: %{ - "end" => %{"character" => 12, "line" => 2}, - "start" => %{"character" => 12, "line" => 2} - } - }, containerName: "# unknown" } ]} = DocumentSymbols.symbols(uri, text, false) @@ -778,25 +667,17 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do %Protocol.DocumentSymbol{ children: [], kind: 12, - name: "def my_fn()", - range: %{ - "end" => %{"character" => 12, "line" => 2}, - "start" => %{"character" => 12, "line" => 2} - }, - selectionRange: %{ - "end" => %{"character" => 12, "line" => 2}, - "start" => %{"character" => 12, "line" => 2} - } + name: "def my_fn()" } ], kind: 2, name: "my_module", range: %{ - "end" => %{"character" => 6, "line" => 1}, + "end" => %{"character" => 9, "line" => 3}, "start" => %{"character" => 6, "line" => 1} }, selectionRange: %{ - "end" => %{"character" => 6, "line" => 1}, + "end" => %{"character" => 9, "line" => 3}, "start" => %{"character" => 6, "line" => 1} } } @@ -818,7 +699,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do kind: 2, location: %{ range: %{ - "end" => %{"character" => 6, "line" => 1}, + "end" => %{"character" => 9, "line" => 3}, "start" => %{"character" => 6, "line" => 1} } } @@ -826,12 +707,6 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do %Protocol.SymbolInformation{ name: "def my_fn()", kind: 12, - location: %{ - range: %{ - "end" => %{"character" => 12, "line" => 2}, - "start" => %{"character" => 12, "line" => 2} - } - }, containerName: "my_module" } ]} = DocumentSymbols.symbols(uri, text, false) @@ -856,39 +731,15 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do %Protocol.DocumentSymbol{ children: [], kind: 12, - name: "def my_fn()", - range: %{ - "end" => %{"character" => 14, "line" => 3}, - "start" => %{"character" => 14, "line" => 3} - }, - selectionRange: %{ - "end" => %{"character" => 14, "line" => 3}, - "start" => %{"character" => 14, "line" => 3} - } + name: "def my_fn()" } ], kind: 2, - name: "__MODULE__.SubModule", - range: %{ - "end" => %{"character" => 8, "line" => 2}, - "start" => %{"character" => 8, "line" => 2} - }, - selectionRange: %{ - "end" => %{"character" => 8, "line" => 2}, - "start" => %{"character" => 8, "line" => 2} - } + name: "__MODULE__.SubModule" } ], kind: 2, - name: "__MODULE__", - range: %{ - "end" => %{"character" => 6, "line" => 1}, - "start" => %{"character" => 6, "line" => 1} - }, - selectionRange: %{ - "end" => %{"character" => 6, "line" => 1}, - "start" => %{"character" => 6, "line" => 1} - } + name: "__MODULE__" } ]} = DocumentSymbols.symbols(uri, text, true) end @@ -907,34 +758,16 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do [ %Protocol.SymbolInformation{ name: "__MODULE__", - kind: 2, - location: %{ - range: %{ - "end" => %{"character" => 6, "line" => 1}, - "start" => %{"character" => 6, "line" => 1} - } - } + kind: 2 }, %Protocol.SymbolInformation{ name: "__MODULE__.SubModule", kind: 2, - location: %{ - range: %{ - "end" => %{"character" => 8, "line" => 2}, - "start" => %{"character" => 8, "line" => 2} - } - }, containerName: "__MODULE__" }, %Protocol.SymbolInformation{ name: "def my_fn()", kind: 12, - location: %{ - range: %{ - "end" => %{"character" => 14, "line" => 3}, - "start" => %{"character" => 14, "line" => 3} - } - }, containerName: "__MODULE__.SubModule" } ]} = DocumentSymbols.symbols(uri, text, false) @@ -967,11 +800,11 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do kind: 12, name: "def size(data)", range: %{ - "end" => %{"character" => 6, "line" => 2}, - "start" => %{"character" => 6, "line" => 2} + "end" => %{"character" => 2, "line" => 2}, + "start" => %{"character" => 2, "line" => 2} }, selectionRange: %{ - "end" => %{"character" => 6, "line" => 2}, + "end" => %{"character" => 16, "line" => 2}, "start" => %{"character" => 6, "line" => 2} } } @@ -979,12 +812,12 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do kind: 11, name: "MyProtocol", range: %{ - "end" => %{"character" => 0, "line" => 0}, + "end" => %{"character" => 3, "line" => 3}, "start" => %{"character" => 0, "line" => 0} }, selectionRange: %{ - "end" => %{"character" => 0, "line" => 0}, - "start" => %{"character" => 0, "line" => 0} + "end" => %{"character" => 12, "line" => 0}, + "start" => %{"character" => 12, "line" => 0} } }, %Protocol.DocumentSymbol{ @@ -994,11 +827,11 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do kind: 12, name: "def size(binary)", range: %{ - "end" => %{"character" => 6, "line" => 6}, - "start" => %{"character" => 6, "line" => 6} + "end" => %{"character" => 2, "line" => 6}, + "start" => %{"character" => 2, "line" => 6} }, selectionRange: %{ - "end" => %{"character" => 6, "line" => 6}, + "end" => %{"character" => 18, "line" => 6}, "start" => %{"character" => 6, "line" => 6} } } @@ -1006,11 +839,11 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do kind: 2, name: "MyProtocol, for: BitString", range: %{ - "end" => %{"character" => 0, "line" => 5}, + "end" => %{"character" => 3, "line" => 7}, "start" => %{"character" => 0, "line" => 5} }, selectionRange: %{ - "end" => %{"character" => 0, "line" => 5}, + "end" => %{"character" => 3, "line" => 7}, "start" => %{"character" => 0, "line" => 5} } }, @@ -1021,11 +854,11 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do kind: 12, name: "def size(param)", range: %{ - "end" => %{"character" => 6, "line" => 10}, - "start" => %{"character" => 6, "line" => 10} + "end" => %{"character" => 2, "line" => 10}, + "start" => %{"character" => 2, "line" => 10} }, selectionRange: %{ - "end" => %{"character" => 6, "line" => 10}, + "end" => %{"character" => 17, "line" => 10}, "start" => %{"character" => 6, "line" => 10} } } @@ -1033,11 +866,11 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do kind: 2, name: "MyProtocol, for: [List, MyList]", range: %{ - "end" => %{"character" => 0, "line" => 9}, + "end" => %{"character" => 3, "line" => 11}, "start" => %{"character" => 0, "line" => 9} }, selectionRange: %{ - "end" => %{"character" => 0, "line" => 9}, + "end" => %{"character" => 3, "line" => 11}, "start" => %{"character" => 0, "line" => 9} } } @@ -1069,7 +902,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do kind: 11, location: %{ range: %{ - "end" => %{"character" => 0, "line" => 0}, + "end" => %{"character" => 3, "line" => 3}, "start" => %{"character" => 0, "line" => 0} } } @@ -1079,8 +912,8 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do name: "def size(data)", location: %{ range: %{ - "end" => %{"character" => 6, "line" => 2}, - "start" => %{"character" => 6, "line" => 2} + "end" => %{"character" => 2, "line" => 2}, + "start" => %{"character" => 2, "line" => 2} } }, containerName: "MyProtocol" @@ -1090,7 +923,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do name: "MyProtocol, for: BitString", location: %{ range: %{ - "end" => %{"character" => 0, "line" => 5}, + "end" => %{"character" => 3, "line" => 7}, "start" => %{"character" => 0, "line" => 5} } } @@ -1100,8 +933,8 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do name: "def size(binary)", location: %{ range: %{ - "end" => %{"character" => 6, "line" => 6}, - "start" => %{"character" => 6, "line" => 6} + "end" => %{"character" => 2, "line" => 6}, + "start" => %{"character" => 2, "line" => 6} } }, containerName: "MyProtocol, for: BitString" @@ -1111,7 +944,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do name: "MyProtocol, for: [List, MyList]", location: %{ range: %{ - "end" => %{"character" => 0, "line" => 9}, + "end" => %{"character" => 3, "line" => 11}, "start" => %{"character" => 0, "line" => 9} } } @@ -1121,8 +954,8 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do name: "def size(param)", location: %{ range: %{ - "end" => %{"character" => 6, "line" => 10}, - "start" => %{"character" => 6, "line" => 10} + "end" => %{"character" => 2, "line" => 10}, + "start" => %{"character" => 2, "line" => 10} } }, containerName: "MyProtocol, for: [List, MyList]" @@ -1185,15 +1018,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do } ], kind: 2, - name: "MyModule", - range: %{ - "end" => %{"character" => 0, "line" => 0}, - "start" => %{"character" => 0, "line" => 0} - }, - selectionRange: %{ - "end" => %{"character" => 0, "line" => 0}, - "start" => %{"character" => 0, "line" => 0} - } + name: "MyModule" } ]} = DocumentSymbols.symbols(uri, text, true) end @@ -1211,13 +1036,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do [ %Protocol.SymbolInformation{ name: "MyModule", - kind: 2, - location: %{ - range: %{ - "end" => %{"character" => 0, "line" => 0}, - "start" => %{"character" => 0, "line" => 0} - } - } + kind: 2 }, %Protocol.SymbolInformation{ name: "defstruct MyModule", @@ -1297,15 +1116,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do } ], kind: 2, - name: "MyError", - range: %{ - "end" => %{"character" => 0, "line" => 0}, - "start" => %{"character" => 0, "line" => 0} - }, - selectionRange: %{ - "end" => %{"character" => 0, "line" => 0}, - "start" => %{"character" => 0, "line" => 0} - } + name: "MyError" } ]} = DocumentSymbols.symbols(uri, text, true) end @@ -1323,13 +1134,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do [ %Protocol.SymbolInformation{ name: "MyError", - kind: 2, - location: %{ - range: %{ - "end" => %{"character" => 0, "line" => 0}, - "start" => %{"character" => 0, "line" => 0} - } - } + kind: 2 }, %Protocol.SymbolInformation{ kind: 23, @@ -1379,90 +1184,42 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do kind: 5, name: "@type my_simple", range: %{ - "end" => %{"character" => 3, "line" => 1}, - "start" => %{"character" => 3, "line" => 1} + "end" => %{"character" => 28, "line" => 1}, + "start" => %{"character" => 2, "line" => 1} }, selectionRange: %{ - "end" => %{"character" => 3, "line" => 1}, - "start" => %{"character" => 3, "line" => 1} + "end" => %{"character" => 8, "line" => 1}, + "start" => %{"character" => 8, "line" => 1} } }, %Protocol.DocumentSymbol{ children: [], kind: 5, - name: "@type my_union", - range: %{ - "end" => %{"character" => 3, "line" => 2}, - "start" => %{"character" => 3, "line" => 2} - }, - selectionRange: %{ - "end" => %{"character" => 3, "line" => 2}, - "start" => %{"character" => 3, "line" => 2} - } + name: "@type my_union" }, %Protocol.DocumentSymbol{ children: [], kind: 5, - name: "@typep my_simple_private", - range: %{ - "end" => %{"character" => 3, "line" => 3}, - "start" => %{"character" => 3, "line" => 3} - }, - selectionRange: %{ - "end" => %{"character" => 3, "line" => 3}, - "start" => %{"character" => 3, "line" => 3} - } + name: "@typep my_simple_private" }, %Protocol.DocumentSymbol{ children: [], kind: 5, - name: "@opaque my_simple_opaque", - range: %{ - "end" => %{"character" => 3, "line" => 4}, - "start" => %{"character" => 3, "line" => 4} - }, - selectionRange: %{ - "end" => %{"character" => 3, "line" => 4}, - "start" => %{"character" => 3, "line" => 4} - } + name: "@opaque my_simple_opaque" }, %Protocol.DocumentSymbol{ children: [], kind: 5, - name: "@type my_with_args(key, value)", - range: %{ - "end" => %{"character" => 3, "line" => 5}, - "start" => %{"character" => 3, "line" => 5} - }, - selectionRange: %{ - "end" => %{"character" => 3, "line" => 5}, - "start" => %{"character" => 3, "line" => 5} - } + name: "@type my_with_args(key, value)" }, %Protocol.DocumentSymbol{ children: [], kind: 5, - name: "@type my_with_args_when(key, value)", - range: %{ - "end" => %{"character" => 3, "line" => 6}, - "start" => %{"character" => 3, "line" => 6} - }, - selectionRange: %{ - "end" => %{"character" => 3, "line" => 6}, - "start" => %{"character" => 3, "line" => 6} - } + name: "@type my_with_args_when(key, value)" } ], kind: 2, - name: "MyModule", - range: %{ - "end" => %{"character" => 0, "line" => 0}, - "start" => %{"character" => 0, "line" => 0} - }, - selectionRange: %{ - "end" => %{"character" => 0, "line" => 0}, - "start" => %{"character" => 0, "line" => 0} - } + name: "MyModule" } ]} = DocumentSymbols.symbols(uri, text, true) end @@ -1485,21 +1242,15 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do [ %Protocol.SymbolInformation{ name: "MyModule", - kind: 2, - location: %{ - range: %{ - "end" => %{"character" => 0, "line" => 0}, - "start" => %{"character" => 0, "line" => 0} - } - } + kind: 2 }, %Protocol.SymbolInformation{ kind: 5, name: "@type my_simple", location: %{ range: %{ - "end" => %{"character" => 3, "line" => 1}, - "start" => %{"character" => 3, "line" => 1} + "end" => %{"character" => 28, "line" => 1}, + "start" => %{"character" => 2, "line" => 1} } }, containerName: "MyModule" @@ -1507,56 +1258,26 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do %Protocol.SymbolInformation{ kind: 5, name: "@type my_union", - location: %{ - range: %{ - "end" => %{"character" => 3, "line" => 2}, - "start" => %{"character" => 3, "line" => 2} - } - }, containerName: "MyModule" }, %Protocol.SymbolInformation{ kind: 5, name: "@typep my_simple_private", - location: %{ - range: %{ - "end" => %{"character" => 3, "line" => 3}, - "start" => %{"character" => 3, "line" => 3} - } - }, containerName: "MyModule" }, %Protocol.SymbolInformation{ kind: 5, name: "@opaque my_simple_opaque", - location: %{ - range: %{ - "end" => %{"character" => 3, "line" => 4}, - "start" => %{"character" => 3, "line" => 4} - } - }, containerName: "MyModule" }, %Protocol.SymbolInformation{ kind: 5, name: "@type my_with_args(key, value)", - location: %{ - range: %{ - "end" => %{"character" => 3, "line" => 5}, - "start" => %{"character" => 3, "line" => 5} - } - }, containerName: "MyModule" }, %Protocol.SymbolInformation{ kind: 5, name: "@type my_with_args_when(key, value)", - location: %{ - range: %{ - "end" => %{"character" => 3, "line" => 6}, - "start" => %{"character" => 3, "line" => 6} - } - }, containerName: "MyModule" } ]} = DocumentSymbols.symbols(uri, text, false) @@ -1587,90 +1308,42 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do kind: 24, name: "@callback my_callback(type1, type2)", range: %{ - "end" => %{"character" => 3, "line" => 1}, - "start" => %{"character" => 3, "line" => 1} + "end" => %{"character" => 52, "line" => 1}, + "start" => %{"character" => 2, "line" => 1} }, selectionRange: %{ - "end" => %{"character" => 3, "line" => 1}, - "start" => %{"character" => 3, "line" => 1} + "end" => %{"character" => 37, "line" => 1}, + "start" => %{"character" => 12, "line" => 1} } }, %Protocol.DocumentSymbol{ children: [], kind: 24, - name: "@macrocallback my_macrocallback(type1, type2)", - range: %{ - "end" => %{"character" => 3, "line" => 2}, - "start" => %{"character" => 3, "line" => 2} - }, - selectionRange: %{ - "end" => %{"character" => 3, "line" => 2}, - "start" => %{"character" => 3, "line" => 2} - } + name: "@macrocallback my_macrocallback(type1, type2)" }, %Protocol.DocumentSymbol{ children: [], kind: 24, - name: "@callback my_callback_when(type1, type2)", - range: %{ - "end" => %{"character" => 3, "line" => 4}, - "start" => %{"character" => 3, "line" => 4} - }, - selectionRange: %{ - "end" => %{"character" => 3, "line" => 4}, - "start" => %{"character" => 3, "line" => 4} - } + name: "@callback my_callback_when(type1, type2)" }, %Protocol.DocumentSymbol{ children: [], kind: 24, - name: "@macrocallback my_macrocallback_when(type1, type2)", - range: %{ - "end" => %{"character" => 3, "line" => 5}, - "start" => %{"character" => 3, "line" => 5} - }, - selectionRange: %{ - "end" => %{"character" => 3, "line" => 5}, - "start" => %{"character" => 3, "line" => 5} - } + name: "@macrocallback my_macrocallback_when(type1, type2)" }, %Protocol.DocumentSymbol{ children: [], kind: 24, - name: "@callback my_callback_no_arg()", - range: %{ - "end" => %{"character" => 3, "line" => 7}, - "start" => %{"character" => 3, "line" => 7} - }, - selectionRange: %{ - "end" => %{"character" => 3, "line" => 7}, - "start" => %{"character" => 3, "line" => 7} - } + name: "@callback my_callback_no_arg()" }, %Protocol.DocumentSymbol{ children: [], kind: 24, - name: "@macrocallback my_macrocallback_no_arg()", - range: %{ - "end" => %{"character" => 3, "line" => 8}, - "start" => %{"character" => 3, "line" => 8} - }, - selectionRange: %{ - "end" => %{"character" => 3, "line" => 8}, - "start" => %{"character" => 3, "line" => 8} - } + name: "@macrocallback my_macrocallback_no_arg()" } ], kind: 2, - name: "MyModule", - range: %{ - "end" => %{"character" => 0, "line" => 0}, - "start" => %{"character" => 0, "line" => 0} - }, - selectionRange: %{ - "end" => %{"character" => 0, "line" => 0}, - "start" => %{"character" => 0, "line" => 0} - } + name: "MyModule" } ]} = DocumentSymbols.symbols(uri, text, true) end @@ -1695,21 +1368,15 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do [ %Protocol.SymbolInformation{ name: "MyModule", - kind: 2, - location: %{ - range: %{ - "end" => %{"character" => 0, "line" => 0}, - "start" => %{"character" => 0, "line" => 0} - } - } + kind: 2 }, %Protocol.SymbolInformation{ name: "@callback my_callback(type1, type2)", kind: 24, location: %{ range: %{ - "end" => %{"character" => 3, "line" => 1}, - "start" => %{"character" => 3, "line" => 1} + "end" => %{"character" => 52, "line" => 1}, + "start" => %{"character" => 2, "line" => 1} } }, containerName: "MyModule" @@ -1717,56 +1384,26 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do %Protocol.SymbolInformation{ name: "@macrocallback my_macrocallback(type1, type2)", kind: 24, - location: %{ - range: %{ - "end" => %{"character" => 3, "line" => 2}, - "start" => %{"character" => 3, "line" => 2} - } - }, containerName: "MyModule" }, %Protocol.SymbolInformation{ name: "@callback my_callback_when(type1, type2)", kind: 24, - location: %{ - range: %{ - "end" => %{"character" => 3, "line" => 4}, - "start" => %{"character" => 3, "line" => 4} - } - }, containerName: "MyModule" }, %Protocol.SymbolInformation{ name: "@macrocallback my_macrocallback_when(type1, type2)", kind: 24, - location: %{ - range: %{ - "end" => %{"character" => 3, "line" => 5}, - "start" => %{"character" => 3, "line" => 5} - } - }, containerName: "MyModule" }, %Protocol.SymbolInformation{ name: "@callback my_callback_no_arg()", kind: 24, - location: %{ - range: %{ - "end" => %{"character" => 3, "line" => 7}, - "start" => %{"character" => 3, "line" => 7} - } - }, containerName: "MyModule" }, %Protocol.SymbolInformation{ name: "@macrocallback my_macrocallback_no_arg()", kind: 24, - location: %{ - range: %{ - "end" => %{"character" => 3, "line" => 8}, - "start" => %{"character" => 3, "line" => 8} - } - }, containerName: "MyModule" } ]} = DocumentSymbols.symbols(uri, text, false) @@ -1788,27 +1425,11 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do %Protocol.DocumentSymbol{ children: [], kind: 12, - name: "def my_fn(a)", - range: %{ - "end" => %{"character" => 12, "line" => 3}, - "start" => %{"character" => 12, "line" => 3} - }, - selectionRange: %{ - "end" => %{"character" => 12, "line" => 3}, - "start" => %{"character" => 12, "line" => 3} - } + name: "def my_fn(a)" } ], kind: 2, - name: "MyModule", - range: %{ - "end" => %{"character" => 6, "line" => 1}, - "start" => %{"character" => 6, "line" => 1} - }, - selectionRange: %{ - "end" => %{"character" => 6, "line" => 1}, - "start" => %{"character" => 6, "line" => 1} - } + name: "MyModule" } ]} = DocumentSymbols.symbols(uri, text, true) end @@ -1826,23 +1447,11 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do [ %Protocol.SymbolInformation{ name: "MyModule", - kind: 2, - location: %{ - range: %{ - "end" => %{"character" => 6, "line" => 1}, - "start" => %{"character" => 6, "line" => 1} - } - } + kind: 2 }, %Protocol.SymbolInformation{ name: "def my_fn(a)", kind: 12, - location: %{ - range: %{ - "end" => %{"character" => 12, "line" => 3}, - "start" => %{"character" => 12, "line" => 3} - } - }, containerName: "MyModule" } ]} = DocumentSymbols.symbols(uri, text, false) @@ -1862,29 +1471,48 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do %Protocol.DocumentSymbol{ children: [ %Protocol.DocumentSymbol{ - children: [], + children: [ + %Protocol.DocumentSymbol{ + children: [], + kind: 7, + name: "name", + range: %{ + "end" => %{"character" => 55, "line" => 3}, + "start" => %{"character" => 15, "line" => 3} + }, + selectionRange: %{ + "end" => %{"character" => 55, "line" => 3}, + "start" => %{"character" => 15, "line" => 3} + } + }, + %Protocol.DocumentSymbol{ + children: [], + kind: 7, + name: "age", + range: %{ + "end" => %{"character" => 55, "line" => 3}, + "start" => %{"character" => 15, "line" => 3} + }, + selectionRange: %{ + "end" => %{"character" => 55, "line" => 3}, + "start" => %{"character" => 15, "line" => 3} + } + } + ], kind: 5, name: "defrecord :user", range: %{ - "end" => %{"character" => 14, "line" => 3}, - "start" => %{"character" => 14, "line" => 3} + "end" => %{"character" => 55, "line" => 3}, + "start" => %{"character" => 8, "line" => 3} }, selectionRange: %{ - "end" => %{"character" => 14, "line" => 3}, - "start" => %{"character" => 14, "line" => 3} + "end" => %{"character" => 55, "line" => 3}, + "start" => %{"character" => 8, "line" => 3} } } ], kind: 2, - name: "MyModule", - range: %{ - "end" => %{"character" => 6, "line" => 1}, - "start" => %{"character" => 6, "line" => 1} - }, - selectionRange: %{ - "end" => %{"character" => 6, "line" => 1}, - "start" => %{"character" => 6, "line" => 1} - } + name: "MyModule" } ]} = DocumentSymbols.symbols(uri, text, true) end @@ -1905,7 +1533,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do kind: 2, location: %{ range: %{ - "end" => %{"character" => 6, "line" => 1}, + "end" => %{"character" => 9, "line" => 4}, "start" => %{"character" => 6, "line" => 1} } } @@ -1913,13 +1541,31 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do %Protocol.SymbolInformation{ name: "defrecord :user", kind: 5, + containerName: "MyModule" + }, + %Protocol.SymbolInformation{ + containerName: "defrecord :user", + kind: 7, location: %{ range: %{ - "end" => %{"character" => 14, "line" => 3}, - "start" => %{"character" => 14, "line" => 3} - } + "end" => %{"character" => 55, "line" => 3}, + "start" => %{"character" => 15, "line" => 3} + }, + uri: "file:///project/file.ex" }, - containerName: "MyModule" + name: "name" + }, + %Protocol.SymbolInformation{ + containerName: "defrecord :user", + kind: 7, + location: %{ + range: %{ + "end" => %{"character" => 55, "line" => 3}, + "start" => %{"character" => 15, "line" => 3} + }, + uri: "file:///project/file.ex" + }, + name: "age" } ]} = DocumentSymbols.symbols(uri, text, false) end @@ -1940,15 +1586,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do %Protocol.DocumentSymbol{ children: [], kind: 2, - name: "MyModule", - range: %{ - "end" => %{"character" => 0, "line" => 0}, - "start" => %{"character" => 0, "line" => 0} - }, - selectionRange: %{ - "end" => %{"character" => 0, "line" => 0}, - "start" => %{"character" => 0, "line" => 0} - } + name: "MyModule" } ]} = DocumentSymbols.symbols(uri, text, true) end @@ -1968,13 +1606,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do [ %Protocol.SymbolInformation{ name: "MyModule", - kind: 2, - location: %{ - range: %{ - "end" => %{"character" => 0, "line" => 0}, - "start" => %{"character" => 0, "line" => 0} - } - } + kind: 2 } ]} = DocumentSymbols.symbols(uri, text, false) end @@ -2013,194 +1645,82 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do kind: 14, name: "@optional_callbacks", range: %{ - "end" => %{"character" => 3, "line" => 1}, - "start" => %{"character" => 3, "line" => 1} + "end" => %{"character" => 58, "line" => 1}, + "start" => %{"character" => 2, "line" => 1} }, selectionRange: %{ - "end" => %{"character" => 3, "line" => 1}, - "start" => %{"character" => 3, "line" => 1} + "end" => %{"character" => 58, "line" => 1}, + "start" => %{"character" => 2, "line" => 1} } }, %Protocol.DocumentSymbol{ children: [], kind: 11, - name: "@behaviour MyBehaviour", - range: %{ - "end" => %{"character" => 3, "line" => 2}, - "start" => %{"character" => 3, "line" => 2} - }, - selectionRange: %{ - "end" => %{"character" => 3, "line" => 2}, - "start" => %{"character" => 3, "line" => 2} - } + name: "@behaviour MyBehaviour" }, %Protocol.DocumentSymbol{ children: [], kind: 14, - name: "@derive", - range: %{ - "end" => %{"character" => 3, "line" => 4}, - "start" => %{"character" => 3, "line" => 4} - }, - selectionRange: %{ - "end" => %{"character" => 3, "line" => 4}, - "start" => %{"character" => 3, "line" => 4} - } + name: "@derive" }, %Protocol.DocumentSymbol{ children: [], kind: 14, - name: "@enforce_keys", - range: %{ - "end" => %{"character" => 3, "line" => 5}, - "start" => %{"character" => 3, "line" => 5} - }, - selectionRange: %{ - "end" => %{"character" => 3, "line" => 5}, - "start" => %{"character" => 3, "line" => 5} - } + name: "@enforce_keys" }, %Protocol.DocumentSymbol{ children: [], kind: 14, - name: "@compile", - range: %{ - "end" => %{"character" => 3, "line" => 6}, - "start" => %{"character" => 3, "line" => 6} - }, - selectionRange: %{ - "end" => %{"character" => 3, "line" => 6}, - "start" => %{"character" => 3, "line" => 6} - } + name: "@compile" }, %Protocol.DocumentSymbol{ children: [], kind: 14, - name: "@dialyzer", - range: %{ - "end" => %{"character" => 3, "line" => 8}, - "start" => %{"character" => 3, "line" => 8} - }, - selectionRange: %{ - "end" => %{"character" => 3, "line" => 8}, - "start" => %{"character" => 3, "line" => 8} - } + name: "@dialyzer" }, %Protocol.DocumentSymbol{ children: [], kind: 14, - name: "@file", - range: %{ - "end" => %{"character" => 3, "line" => 9}, - "start" => %{"character" => 3, "line" => 9} - }, - selectionRange: %{ - "end" => %{"character" => 3, "line" => 9}, - "start" => %{"character" => 3, "line" => 9} - } + name: "@file" }, %Protocol.DocumentSymbol{ children: [], kind: 14, - name: "@external_resource", - range: %{ - "end" => %{"character" => 3, "line" => 10}, - "start" => %{"character" => 3, "line" => 10} - }, - selectionRange: %{ - "end" => %{"character" => 3, "line" => 10}, - "start" => %{"character" => 3, "line" => 10} - } + name: "@external_resource" }, %Protocol.DocumentSymbol{ children: [], kind: 14, - name: "@on_load", - range: %{ - "end" => %{"character" => 3, "line" => 11}, - "start" => %{"character" => 3, "line" => 11} - }, - selectionRange: %{ - "end" => %{"character" => 3, "line" => 11}, - "start" => %{"character" => 3, "line" => 11} - } + name: "@on_load" }, %Protocol.DocumentSymbol{ children: [], kind: 14, - name: "@on_definition", - range: %{ - "end" => %{"character" => 3, "line" => 12}, - "start" => %{"character" => 3, "line" => 12} - }, - selectionRange: %{ - "end" => %{"character" => 3, "line" => 12}, - "start" => %{"character" => 3, "line" => 12} - } + name: "@on_definition" }, %Protocol.DocumentSymbol{ children: [], kind: 14, - name: "@vsn", - range: %{ - "end" => %{"character" => 3, "line" => 13}, - "start" => %{"character" => 3, "line" => 13} - }, - selectionRange: %{ - "end" => %{"character" => 3, "line" => 13}, - "start" => %{"character" => 3, "line" => 13} - } + name: "@vsn" }, %Protocol.DocumentSymbol{ children: [], kind: 14, - name: "@after_compile", - range: %{ - "end" => %{"character" => 3, "line" => 14}, - "start" => %{"character" => 3, "line" => 14} - }, - selectionRange: %{ - "end" => %{"character" => 3, "line" => 14}, - "start" => %{"character" => 3, "line" => 14} - } + name: "@after_compile" }, %Protocol.DocumentSymbol{ children: [], kind: 14, - name: "@before_compile", - range: %{ - "end" => %{"character" => 3, "line" => 15}, - "start" => %{"character" => 3, "line" => 15} - }, - selectionRange: %{ - "end" => %{"character" => 3, "line" => 15}, - "start" => %{"character" => 3, "line" => 15} - } + name: "@before_compile" }, %Protocol.DocumentSymbol{ children: [], kind: 14, - name: "@fallback_to_any", - range: %{ - "end" => %{"character" => 3, "line" => 16}, - "start" => %{"character" => 3, "line" => 16} - }, - selectionRange: %{ - "end" => %{"character" => 3, "line" => 16}, - "start" => %{"character" => 3, "line" => 16} - } + name: "@fallback_to_any" } ], kind: 2, - name: "MyModule", - range: %{ - "end" => %{"character" => 0, "line" => 0}, - "start" => %{"character" => 0, "line" => 0} - }, - selectionRange: %{ - "end" => %{"character" => 0, "line" => 0}, - "start" => %{"character" => 0, "line" => 0} - } + name: "MyModule" } ]} = DocumentSymbols.symbols(uri, text, true) end @@ -2237,7 +1757,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do kind: 2, location: %{ range: %{ - "end" => %{"character" => 0, "line" => 0}, + "end" => %{"character" => 3, "line" => 18}, "start" => %{"character" => 0, "line" => 0} } } @@ -2247,8 +1767,8 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do kind: 14, location: %{ range: %{ - "end" => %{"character" => 3, "line" => 1}, - "start" => %{"character" => 3, "line" => 1} + "end" => %{"character" => 58, "line" => 1}, + "start" => %{"character" => 2, "line" => 1} } }, containerName: "MyModule" @@ -2256,144 +1776,66 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do %Protocol.SymbolInformation{ name: "@behaviour MyBehaviour", kind: 11, - location: %{ - range: %{ - "end" => %{"character" => 3, "line" => 2}, - "start" => %{"character" => 3, "line" => 2} - } - }, containerName: "MyModule" }, %Protocol.SymbolInformation{ name: "@derive", kind: 14, - location: %{ - range: %{ - "end" => %{"character" => 3, "line" => 4}, - "start" => %{"character" => 3, "line" => 4} - } - }, containerName: "MyModule" }, %Protocol.SymbolInformation{ name: "@enforce_keys", kind: 14, - location: %{ - range: %{ - "end" => %{"character" => 3, "line" => 5}, - "start" => %{"character" => 3, "line" => 5} - } - }, containerName: "MyModule" }, %Protocol.SymbolInformation{ name: "@compile", kind: 14, - location: %{ - range: %{ - "end" => %{"character" => 3, "line" => 6}, - "start" => %{"character" => 3, "line" => 6} - } - }, containerName: "MyModule" }, %Protocol.SymbolInformation{ name: "@dialyzer", kind: 14, - location: %{ - range: %{ - "end" => %{"character" => 3, "line" => 8}, - "start" => %{"character" => 3, "line" => 8} - } - }, containerName: "MyModule" }, %Protocol.SymbolInformation{ name: "@file", kind: 14, - location: %{ - range: %{ - "end" => %{"character" => 3, "line" => 9}, - "start" => %{"character" => 3, "line" => 9} - } - }, containerName: "MyModule" }, %Protocol.SymbolInformation{ name: "@external_resource", kind: 14, - location: %{ - range: %{ - "end" => %{"character" => 3, "line" => 10}, - "start" => %{"character" => 3, "line" => 10} - } - }, containerName: "MyModule" }, %Protocol.SymbolInformation{ name: "@on_load", kind: 14, - location: %{ - range: %{ - "end" => %{"character" => 3, "line" => 11}, - "start" => %{"character" => 3, "line" => 11} - } - }, containerName: "MyModule" }, %Protocol.SymbolInformation{ name: "@on_definition", kind: 14, - location: %{ - range: %{ - "end" => %{"character" => 3, "line" => 12}, - "start" => %{"character" => 3, "line" => 12} - } - }, containerName: "MyModule" }, %Protocol.SymbolInformation{ name: "@vsn", kind: 14, - location: %{ - range: %{ - "end" => %{"character" => 3, "line" => 13}, - "start" => %{"character" => 3, "line" => 13} - } - }, containerName: "MyModule" }, %Protocol.SymbolInformation{ name: "@after_compile", kind: 14, - location: %{ - range: %{ - "end" => %{"character" => 3, "line" => 14}, - "start" => %{"character" => 3, "line" => 14} - } - }, containerName: "MyModule" }, %Protocol.SymbolInformation{ name: "@before_compile", kind: 14, - location: %{ - range: %{ - "end" => %{"character" => 3, "line" => 15}, - "start" => %{"character" => 3, "line" => 15} - } - }, containerName: "MyModule" }, %Protocol.SymbolInformation{ name: "@fallback_to_any", kind: 14, - location: %{ - range: %{ - "end" => %{"character" => 3, "line" => 16}, - "start" => %{"character" => 3, "line" => 16} - } - }, containerName: "MyModule" } ]} = DocumentSymbols.symbols(uri, text, false) @@ -2429,12 +1871,12 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do kind: 2, name: "MyModuleTest", range: %{ - "end" => %{"character" => 6, "line" => 1}, + "end" => %{"character" => 9, "line" => 4}, "start" => %{"character" => 6, "line" => 1} }, selectionRange: %{ - "end" => %{"character" => 6, "line" => 1}, - "start" => %{"character" => 6, "line" => 1} + "end" => %{"character" => 16, "line" => 1}, + "start" => %{"character" => 16, "line" => 1} } } ]} = DocumentSymbols.symbols(uri, text, true) @@ -2456,7 +1898,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do kind: 2, location: %{ range: %{ - "end" => %{"character" => 6, "line" => 1}, + "end" => %{"character" => 9, "line" => 4}, "start" => %{"character" => 6, "line" => 1} } } @@ -2509,11 +1951,11 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do kind: 12, name: "describe \"some description\"", range: %{ - "end" => %{"character" => 8, "line" => 3}, + "end" => %{"character" => 11, "line" => 5}, "start" => %{"character" => 8, "line" => 3} }, selectionRange: %{ - "end" => %{"character" => 8, "line" => 3}, + "end" => %{"character" => 11, "line" => 5}, "start" => %{"character" => 8, "line" => 3} } } @@ -2521,12 +1963,12 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do kind: 2, name: "MyModuleTest", range: %{ - "end" => %{"character" => 6, "line" => 1}, + "end" => %{"character" => 9, "line" => 6}, "start" => %{"character" => 6, "line" => 1} }, selectionRange: %{ - "end" => %{"character" => 6, "line" => 1}, - "start" => %{"character" => 6, "line" => 1} + "end" => %{"character" => 16, "line" => 1}, + "start" => %{"character" => 16, "line" => 1} } } ]} = DocumentSymbols.symbols(uri, text, true) @@ -2566,11 +2008,11 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do kind: 12, name: describe_sigil, range: %{ - "end" => %{"character" => 8, "line" => 3}, + "end" => %{"character" => 11, "line" => 5}, "start" => %{"character" => 8, "line" => 3} }, selectionRange: %{ - "end" => %{"character" => 8, "line" => 3}, + "end" => %{"character" => 11, "line" => 5}, "start" => %{"character" => 8, "line" => 3} } } @@ -2578,21 +2020,17 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do kind: 2, name: "MyModuleTest", range: %{ - "end" => %{"character" => 6, "line" => 1}, + "end" => %{"character" => 9, "line" => 6}, "start" => %{"character" => 6, "line" => 1} }, selectionRange: %{ - "end" => %{"character" => 6, "line" => 1}, - "start" => %{"character" => 6, "line" => 1} + "end" => %{"character" => 16, "line" => 1}, + "start" => %{"character" => 16, "line" => 1} } } ]} = DocumentSymbols.symbols(uri, text, true) - if System.version() |> Version.match?(">= 1.10.0") do - assert describe_sigil == "describe ~S(some \"description\")" - else - assert describe_sigil == "describe ~S'some \"description\"'" - end + assert describe_sigil == "describe ~S(some \"description\")" end test "[flat] handles exunit describe tests" do @@ -2613,7 +2051,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do kind: 2, location: %{ range: %{ - "end" => %{"character" => 6, "line" => 1}, + "end" => %{"character" => 9, "line" => 6}, "start" => %{"character" => 6, "line" => 1} } } @@ -2623,7 +2061,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do kind: 12, location: %{ range: %{ - "end" => %{"character" => 8, "line" => 3}, + "end" => %{"character" => 11, "line" => 5}, "start" => %{"character" => 8, "line" => 3} } }, @@ -2661,7 +2099,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do kind: 2, location: %{ range: %{ - "end" => %{"character" => 6, "line" => 1}, + "end" => %{"character" => 9, "line" => 6}, "start" => %{"character" => 6, "line" => 1} } } @@ -2671,7 +2109,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do kind: 12, location: %{ range: %{ - "end" => %{"character" => 8, "line" => 3}, + "end" => %{"character" => 11, "line" => 5}, "start" => %{"character" => 8, "line" => 3} } }, @@ -2690,11 +2128,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do } ]} = DocumentSymbols.symbols(uri, text, false) - if System.version() |> Version.match?(">= 1.10.0") do - assert describe_sigil == "describe ~S(some \"description\")" - else - assert describe_sigil == "describe ~S'some \"description\"'" - end + assert describe_sigil == "describe ~S(some \"description\")" end test "[nested] handles exunit callbacks" do @@ -2722,11 +2156,11 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do kind: 12, name: "setup", range: %{ - "end" => %{"character" => 2, "line" => 2}, + "end" => %{"character" => 5, "line" => 4}, "start" => %{"character" => 2, "line" => 2} }, selectionRange: %{ - "end" => %{"character" => 2, "line" => 2}, + "end" => %{"character" => 5, "line" => 4}, "start" => %{"character" => 2, "line" => 2} } }, @@ -2735,11 +2169,11 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do kind: 12, name: "setup", range: %{ - "end" => %{"character" => 2, "line" => 5}, + "end" => %{"character" => 31, "line" => 5}, "start" => %{"character" => 2, "line" => 5} }, selectionRange: %{ - "end" => %{"character" => 2, "line" => 5}, + "end" => %{"character" => 31, "line" => 5}, "start" => %{"character" => 2, "line" => 5} } }, @@ -2748,11 +2182,11 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do kind: 12, name: "setup_all", range: %{ - "end" => %{"character" => 2, "line" => 6}, + "end" => %{"character" => 5, "line" => 8}, "start" => %{"character" => 2, "line" => 6} }, selectionRange: %{ - "end" => %{"character" => 2, "line" => 6}, + "end" => %{"character" => 5, "line" => 8}, "start" => %{"character" => 2, "line" => 6} } } @@ -2760,12 +2194,12 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do kind: 2, name: "MyModuleTest", range: %{ - "end" => %{"character" => 0, "line" => 0}, + "end" => %{"character" => 3, "line" => 9}, "start" => %{"character" => 0, "line" => 0} }, selectionRange: %{ - "end" => %{"character" => 0, "line" => 0}, - "start" => %{"character" => 0, "line" => 0} + "end" => %{"character" => 10, "line" => 0}, + "start" => %{"character" => 10, "line" => 0} } } ]} = DocumentSymbols.symbols(uri, text, true) @@ -2794,7 +2228,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do kind: 2, location: %{ range: %{ - "end" => %{"character" => 0, "line" => 0}, + "end" => %{"character" => 3, "line" => 9}, "start" => %{"character" => 0, "line" => 0} } } @@ -2804,7 +2238,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do kind: 12, location: %{ range: %{ - "end" => %{"character" => 2, "line" => 2}, + "end" => %{"character" => 5, "line" => 4}, "start" => %{"character" => 2, "line" => 2} } }, @@ -2815,7 +2249,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do kind: 12, location: %{ range: %{ - "end" => %{"character" => 2, "line" => 5}, + "end" => %{"character" => 31, "line" => 5}, "start" => %{"character" => 2, "line" => 5} } }, @@ -2826,7 +2260,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do kind: 12, location: %{ range: %{ - "end" => %{"character" => 2, "line" => 6}, + "end" => %{"character" => 5, "line" => 8}, "start" => %{"character" => 2, "line" => 6} } }, @@ -2859,11 +2293,11 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do kind: 20, name: "config :logger :console", range: %{ - "end" => %{"character" => 0, "line" => 1}, + "end" => %{"character" => 23, "line" => 5}, "start" => %{"character" => 0, "line" => 1} }, selectionRange: %{ - "end" => %{"character" => 0, "line" => 1}, + "end" => %{"character" => 23, "line" => 5}, "start" => %{"character" => 0, "line" => 1} } }, @@ -2872,24 +2306,24 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do kind: 20, name: "config :app :key", range: %{ - "end" => %{"character" => 0, "line" => 6}, + "end" => %{"character" => 25, "line" => 6}, "start" => %{"character" => 0, "line" => 6} }, selectionRange: %{ - "end" => %{"character" => 0, "line" => 6}, + "end" => %{"character" => 25, "line" => 6}, "start" => %{"character" => 0, "line" => 6} } }, %Protocol.DocumentSymbol{ children: [], kind: 20, - name: "config :my_app :ecto_repos", + name: "config :my_app [:ecto_repos]", range: %{ - "end" => %{"character" => 0, "line" => 7}, + "end" => %{"character" => 26, "line" => 8}, "start" => %{"character" => 0, "line" => 7} }, selectionRange: %{ - "end" => %{"character" => 0, "line" => 7}, + "end" => %{"character" => 26, "line" => 8}, "start" => %{"character" => 0, "line" => 7} } }, @@ -2933,7 +2367,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do kind: 20, location: %{ range: %{ - "end" => %{"character" => 0, "line" => 1}, + "end" => %{"character" => 23, "line" => 5}, "start" => %{"character" => 0, "line" => 1} } } @@ -2943,17 +2377,17 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do kind: 20, location: %{ range: %{ - "end" => %{"character" => 0, "line" => 6}, + "end" => %{"character" => 25, "line" => 6}, "start" => %{"character" => 0, "line" => 6} } } }, %Protocol.SymbolInformation{ - name: "config :my_app :ecto_repos", + name: "config :my_app [:ecto_repos]", kind: 20, location: %{ range: %{ - "end" => %{"character" => 0, "line" => 7}, + "end" => %{"character" => 26, "line" => 8}, "start" => %{"character" => 0, "line" => 7} } } @@ -2986,33 +2420,17 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do %Protocol.DocumentSymbol{ children: children, kind: 2, - name: "MISSING_MODULE_NAME", - range: %{ - "start" => %{"line" => 0, "character" => 0}, - "end" => %{"line" => 0, "character" => 0} - }, - selectionRange: %{ - "start" => %{"line" => 0, "character" => 0}, - "end" => %{"line" => 0, "character" => 0} - } + name: "MISSING_MODULE_NAME" } ] = document_symbols - assert children == [ + assert [ %Protocol.DocumentSymbol{ children: [], kind: 12, - name: "def foo", - range: %{ - "start" => %{"character" => 4, "line" => 1}, - "end" => %{"character" => 4, "line" => 1} - }, - selectionRange: %{ - "start" => %{"character" => 4, "line" => 1}, - "end" => %{"character" => 4, "line" => 1} - } + name: "def foo" } - ] + ] = children end test "[nested] handles a file with a top-level protocol module without a name" do @@ -3025,21 +2443,13 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do assert {:ok, document_symbols} = DocumentSymbols.symbols(uri, text, true) - assert document_symbols == [ + assert [ %Protocol.DocumentSymbol{ children: [], kind: 11, - name: "MISSING_PROTOCOL_NAME", - range: %{ - "start" => %{"line" => 0, "character" => 0}, - "end" => %{"line" => 1, "character" => 3} - }, - selectionRange: %{ - "start" => %{"line" => 0, "character" => 0}, - "end" => %{"line" => 0, "character" => 0} - } + name: "MISSING_PROTOCOL_NAME" } - ] + ] = document_symbols end test "handles a file with compilation errors by returning an empty list" do From 05656fc0d36c9a4dc7db0bd911bc2eeaba97a53c Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sat, 11 Jun 2022 11:44:57 +0200 Subject: [PATCH 030/125] Revert "make function private" This reverts commit 27443d4c8526fdb67f9f66b388fa92a4e727b144. --- apps/language_server/lib/language_server/build.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/language_server/lib/language_server/build.ex b/apps/language_server/lib/language_server/build.ex index 8afccfc70..df27e6d2d 100644 --- a/apps/language_server/lib/language_server/build.ex +++ b/apps/language_server/lib/language_server/build.ex @@ -112,7 +112,7 @@ defmodule ElixirLS.LanguageServer.Build do # it was added in https://github.com/elixir-lsp/elixir-ls/pull/227 # removing it doesn't break tests and I'm not able to reproduce # https://github.com/elixir-lsp/elixir-ls/issues/209 on recent elixir (1.13) - defp load_all_mix_applications do + def load_all_mix_applications do apps = cond do Mix.Project.umbrella?() -> From 6bcb346939eb0a3ed7123ca02e29397c8c8745e1 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sat, 11 Jun 2022 12:56:03 +0200 Subject: [PATCH 031/125] fix tests on elixir < 1.13 --- .../test/providers/document_symbols_test.exs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/apps/language_server/test/providers/document_symbols_test.exs b/apps/language_server/test/providers/document_symbols_test.exs index 22061f119..1564fe83c 100644 --- a/apps/language_server/test/providers/document_symbols_test.exs +++ b/apps/language_server/test/providers/document_symbols_test.exs @@ -1466,6 +1466,10 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do end ] + result = = DocumentSymbols.symbols(uri, text, true) + + # earlier elixir versions return different ranges + if Version.match?(System.version(), ">= 1.13.0") do assert {:ok, [ %Protocol.DocumentSymbol{ @@ -1514,7 +1518,8 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do kind: 2, name: "MyModule" } - ]} = DocumentSymbols.symbols(uri, text, true) + ]} = result + end end test "[flat] handles records" do @@ -1526,6 +1531,10 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do end ] + result = DocumentSymbols.symbols(uri, text, false) + + # earlier elixir versions return different ranges + if Version.match?(System.version(), ">= 1.13.0") do assert {:ok, [ %Protocol.SymbolInformation{ @@ -1567,7 +1576,8 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do }, name: "age" } - ]} = DocumentSymbols.symbols(uri, text, false) + ]} = result + end end test "[nested] skips docs attributes" do From 3bd305b9ab673c393523883a556ad9215b73f4c5 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sat, 11 Jun 2022 13:02:48 +0200 Subject: [PATCH 032/125] fix tests on elixir < 1.13 --- .../test/providers/document_symbols_test.exs | 170 +++++++++--------- 1 file changed, 85 insertions(+), 85 deletions(-) diff --git a/apps/language_server/test/providers/document_symbols_test.exs b/apps/language_server/test/providers/document_symbols_test.exs index 1564fe83c..c0df5e313 100644 --- a/apps/language_server/test/providers/document_symbols_test.exs +++ b/apps/language_server/test/providers/document_symbols_test.exs @@ -1466,60 +1466,60 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do end ] - result = = DocumentSymbols.symbols(uri, text, true) + result = DocumentSymbols.symbols(uri, text, true) # earlier elixir versions return different ranges if Version.match?(System.version(), ">= 1.13.0") do - assert {:ok, - [ - %Protocol.DocumentSymbol{ - children: [ - %Protocol.DocumentSymbol{ - children: [ - %Protocol.DocumentSymbol{ - children: [], - kind: 7, - name: "name", - range: %{ - "end" => %{"character" => 55, "line" => 3}, - "start" => %{"character" => 15, "line" => 3} + assert {:ok, + [ + %Protocol.DocumentSymbol{ + children: [ + %Protocol.DocumentSymbol{ + children: [ + %Protocol.DocumentSymbol{ + children: [], + kind: 7, + name: "name", + range: %{ + "end" => %{"character" => 55, "line" => 3}, + "start" => %{"character" => 15, "line" => 3} + }, + selectionRange: %{ + "end" => %{"character" => 55, "line" => 3}, + "start" => %{"character" => 15, "line" => 3} + } }, - selectionRange: %{ - "end" => %{"character" => 55, "line" => 3}, - "start" => %{"character" => 15, "line" => 3} + %Protocol.DocumentSymbol{ + children: [], + kind: 7, + name: "age", + range: %{ + "end" => %{"character" => 55, "line" => 3}, + "start" => %{"character" => 15, "line" => 3} + }, + selectionRange: %{ + "end" => %{"character" => 55, "line" => 3}, + "start" => %{"character" => 15, "line" => 3} + } } + ], + kind: 5, + name: "defrecord :user", + range: %{ + "end" => %{"character" => 55, "line" => 3}, + "start" => %{"character" => 8, "line" => 3} }, - %Protocol.DocumentSymbol{ - children: [], - kind: 7, - name: "age", - range: %{ - "end" => %{"character" => 55, "line" => 3}, - "start" => %{"character" => 15, "line" => 3} - }, - selectionRange: %{ - "end" => %{"character" => 55, "line" => 3}, - "start" => %{"character" => 15, "line" => 3} - } + selectionRange: %{ + "end" => %{"character" => 55, "line" => 3}, + "start" => %{"character" => 8, "line" => 3} } - ], - kind: 5, - name: "defrecord :user", - range: %{ - "end" => %{"character" => 55, "line" => 3}, - "start" => %{"character" => 8, "line" => 3} - }, - selectionRange: %{ - "end" => %{"character" => 55, "line" => 3}, - "start" => %{"character" => 8, "line" => 3} } - } - ], - kind: 2, - name: "MyModule" - } - ]} = result - end + ], + kind: 2, + name: "MyModule" + } + ]} = result + end end test "[flat] handles records" do @@ -1535,49 +1535,49 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do # earlier elixir versions return different ranges if Version.match?(System.version(), ">= 1.13.0") do - assert {:ok, - [ - %Protocol.SymbolInformation{ - name: "MyModule", - kind: 2, - location: %{ - range: %{ - "end" => %{"character" => 9, "line" => 4}, - "start" => %{"character" => 6, "line" => 1} + assert {:ok, + [ + %Protocol.SymbolInformation{ + name: "MyModule", + kind: 2, + location: %{ + range: %{ + "end" => %{"character" => 9, "line" => 4}, + "start" => %{"character" => 6, "line" => 1} + } } - } - }, - %Protocol.SymbolInformation{ - name: "defrecord :user", - kind: 5, - containerName: "MyModule" - }, - %Protocol.SymbolInformation{ - containerName: "defrecord :user", - kind: 7, - location: %{ - range: %{ - "end" => %{"character" => 55, "line" => 3}, - "start" => %{"character" => 15, "line" => 3} - }, - uri: "file:///project/file.ex" }, - name: "name" - }, - %Protocol.SymbolInformation{ - containerName: "defrecord :user", - kind: 7, - location: %{ - range: %{ - "end" => %{"character" => 55, "line" => 3}, - "start" => %{"character" => 15, "line" => 3} + %Protocol.SymbolInformation{ + name: "defrecord :user", + kind: 5, + containerName: "MyModule" + }, + %Protocol.SymbolInformation{ + containerName: "defrecord :user", + kind: 7, + location: %{ + range: %{ + "end" => %{"character" => 55, "line" => 3}, + "start" => %{"character" => 15, "line" => 3} + }, + uri: "file:///project/file.ex" }, - uri: "file:///project/file.ex" + name: "name" }, - name: "age" - } - ]} = result - end + %Protocol.SymbolInformation{ + containerName: "defrecord :user", + kind: 7, + location: %{ + range: %{ + "end" => %{"character" => 55, "line" => 3}, + "start" => %{"character" => 15, "line" => 3} + }, + uri: "file:///project/file.ex" + }, + name: "age" + } + ]} = result + end end test "[nested] skips docs attributes" do From a5fd8ecbf5a78e0e744a354d391cd1e3e33f9b7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Samson?= Date: Sun, 19 Jun 2022 14:51:44 +0200 Subject: [PATCH 033/125] do not rely on compile OTP version dialyzer internal record (#703) it may differ from running version --- .../lib/language_server/dialyzer/analyzer.ex | 61 ++++++++++++++++--- 1 file changed, 53 insertions(+), 8 deletions(-) diff --git a/apps/language_server/lib/language_server/dialyzer/analyzer.ex b/apps/language_server/lib/language_server/dialyzer/analyzer.ex index 8641c6d0c..5054ef776 100644 --- a/apps/language_server/lib/language_server/dialyzer/analyzer.ex +++ b/apps/language_server/lib/language_server/dialyzer/analyzer.ex @@ -56,7 +56,44 @@ defmodule ElixirLS.LanguageServer.Dialyzer.Analyzer do log_cache: [] ] - Record.defrecordp(:analysis, Record.extract(:analysis, from_lib: "dialyzer/src/dialyzer.hrl")) + Record.defrecordp( + :analysis_24, + :analysis, + analysis_pid: :undefined, + type: :succ_typings, + defines: [], + doc_plt: :undefined, + files: [], + include_dirs: [], + start_from: :byte_code, + plt: :undefined, + use_contracts: true, + race_detection: false, + behaviours_chk: false, + timing: false, + timing_server: :none, + callgraph_file: [], + solvers: :undefined + ) + + Record.defrecordp( + :analysis_25, + :analysis, + analysis_pid: :undefined, + type: :succ_typings, + defines: [], + doc_plt: :undefined, + files: [], + include_dirs: [], + start_from: :byte_code, + plt: :undefined, + use_contracts: true, + behaviours_chk: false, + timing: false, + timing_server: :none, + callgraph_file: [], + solvers: :undefined + ) def analyze(active_plt, []) do {active_plt, %{}, []} @@ -64,13 +101,21 @@ defmodule ElixirLS.LanguageServer.Dialyzer.Analyzer do def analyze(active_plt, files) do analysis_config = - analysis( - plt: active_plt, - files: files, - type: :succ_typings, - start_from: :byte_code, - solvers: [] - ) + case System.otp_release() |> String.to_integer() do + ver when ver < 25 -> + analysis_24( + plt: active_plt, + files: files, + solvers: [] + ) + + _ -> + analysis_25( + plt: active_plt, + files: files, + solvers: [] + ) + end parent = self() From 2db8acc32bc988beadaa8f0fd064bd6770e6152d Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Mon, 20 Jun 2022 21:50:05 +0200 Subject: [PATCH 034/125] improve selection ranges in document symbols never return selection ranges outside ranges --- .../providers/document_symbols.ex | 74 +++++++++++++++++-- .../test/providers/document_symbols_test.exs | 66 +++++------------ 2 files changed, 83 insertions(+), 57 deletions(-) diff --git a/apps/language_server/lib/language_server/providers/document_symbols.ex b/apps/language_server/lib/language_server/providers/document_symbols.ex index 072835600..0a8790e21 100644 --- a/apps/language_server/lib/language_server/providers/document_symbols.ex +++ b/apps/language_server/lib/language_server/providers/document_symbols.ex @@ -10,7 +10,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbols do require ElixirLS.LanguageServer.Protocol, as: Protocol defmodule Info do - defstruct [:type, :name, :location, :children, :selection_location] + defstruct [:type, :name, :location, :children, :selection_location, :symbol] end @defs [:def, :defp, :defmacro, :defmacrop, :defguard, :defguardp, :defdelegate] @@ -92,6 +92,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbols do end # TODO extract module name location from Code.Fragment.surround_context? + # TODO better selection ranges for defimpl? {extract_module_name(module_expression), module_name_location, module_body} end @@ -117,7 +118,8 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbols do name: module_name, location: location, selection_location: module_name_location, - children: module_symbols + children: module_symbols, + symbol: module_name } end @@ -175,12 +177,13 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbols do |> String.replace("\n", "") type = if type_kind in [:type, :typep, :opaque], do: :class, else: :event - # TODO no closing metdata in type expressions + %Info{ type: type, name: "@#{type_kind} #{type_name}", location: location, selection_location: type_head_location, + symbol: "#{type_name}", children: [] } end @@ -207,6 +210,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbols do %Info{ type: :function, + symbol: "#{name}", name: "#{defname} #{name}", location: location, selection_location: head_location, @@ -221,6 +225,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbols do %Info{ type: :function, + symbol: "#{name}", name: "#{defname} #{name}", location: location, selection_location: head_location, @@ -324,15 +329,64 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbols do do: Enum.map(info, &build_symbol_information_hierarchical(uri, text, &1)) defp build_symbol_information_hierarchical(uri, text, %Info{} = info) do + selection_range = + location_to_range(info.selection_location || info.location, text, info.symbol) + + # range must contain selection range + range = + location_to_range(info.location, text, nil) + |> maybe_extend_range(selection_range) + %Protocol.DocumentSymbol{ name: info.name, kind: SymbolUtils.symbol_kind_to_code(info.type), - range: location_to_range(info.location, text), - selectionRange: location_to_range(info.selection_location || info.location, text), + range: range, + selectionRange: selection_range, children: build_symbol_information_hierarchical(uri, text, info.children) } end + defp maybe_extend_range( + Protocol.range(start_line, start_character, end_line, end_character), + Protocol.range( + selection_start_line, + selection_start_character, + selection_end_line, + selection_end_character + ) + ) do + {extended_start_line, extended_start_character} = + cond do + selection_start_line < start_line -> + {selection_start_line, selection_start_character} + + selection_start_line == start_line -> + {selection_start_line, min(selection_start_character, start_character)} + + true -> + {start_line, start_character} + end + + {extended_end_line, extended_end_character} = + cond do + selection_end_line > end_line -> + {selection_end_line, selection_end_character} + + selection_end_line == end_line -> + {selection_end_line, max(selection_end_character, end_character)} + + true -> + {end_line, end_character} + end + + Protocol.range( + extended_start_line, + extended_start_character, + extended_end_line, + extended_end_character + ) + end + defp build_symbol_information_flat(uri, text, info, parent_name \\ nil) defp build_symbol_information_flat(uri, text, info, parent_name) when is_list(info), @@ -347,7 +401,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbols do kind: SymbolUtils.symbol_kind_to_code(info.type), location: %{ uri: uri, - range: location_to_range(info.location, text) + range: location_to_range(info.location, text, nil) }, containerName: parent_name } @@ -360,14 +414,14 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbols do kind: SymbolUtils.symbol_kind_to_code(info.type), location: %{ uri: uri, - range: location_to_range(info.location, text) + range: location_to_range(info.location, text, nil) }, containerName: parent_name } end end - defp location_to_range(location, text) do + defp location_to_range(location, text, symbol) do {start_line, start_character} = SourceFile.elixir_position_to_lsp(text, {location[:line], location[:column]}) @@ -389,6 +443,10 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbols do {end_location[:line], end_location[:column] + 1} ) + symbol != nil -> + end_char = SourceFile.elixir_character_to_lsp(symbol, String.length(symbol)) + {start_line, start_character + end_char + 1} + true -> {start_line, start_character} end diff --git a/apps/language_server/test/providers/document_symbols_test.exs b/apps/language_server/test/providers/document_symbols_test.exs index c0df5e313..1b6c3d5dc 100644 --- a/apps/language_server/test/providers/document_symbols_test.exs +++ b/apps/language_server/test/providers/document_symbols_test.exs @@ -133,7 +133,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do "start" => %{"character" => 8, "line" => 11} }, selectionRange: %{ - "end" => %{"character" => 12, "line" => 11}, + "end" => %{"character" => 24, "line" => 11}, "start" => %{"character" => 12, "line" => 11} } }, @@ -202,7 +202,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do "start" => %{"character" => 6, "line" => 1} }, selectionRange: %{ - "end" => %{"character" => 16, "line" => 1}, + "end" => %{"character" => 24, "line" => 1}, "start" => %{"character" => 16, "line" => 1} } } @@ -347,7 +347,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do uri = "file:///project/file.ex" text = ~S[ defmodule MyModule do - defmodule SubModule do + defmodule Sub.Module do def my_fn(), do: :ok end end @@ -366,13 +366,13 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do } ], kind: 2, - name: "SubModule", + name: "Sub.Module", range: %{ "end" => %{"character" => 11, "line" => 4}, "start" => %{"character" => 8, "line" => 2} }, selectionRange: %{ - "end" => %{"character" => 18, "line" => 2}, + "end" => %{"character" => 28, "line" => 2}, "start" => %{"character" => 18, "line" => 2} } } @@ -384,7 +384,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do "start" => %{"character" => 6, "line" => 1} }, selectionRange: %{ - "end" => %{"character" => 16, "line" => 1}, + "end" => %{"character" => 24, "line" => 1}, "start" => %{"character" => 16, "line" => 1} } } @@ -460,7 +460,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do "start" => %{"character" => 6, "line" => 1} }, selectionRange: %{ - "end" => %{"character" => 16, "line" => 1}, + "end" => %{"character" => 24, "line" => 1}, "start" => %{"character" => 16, "line" => 1} } }, @@ -479,7 +479,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do "start" => %{"character" => 6, "line" => 4} }, selectionRange: %{ - "end" => %{"character" => 16, "line" => 4}, + "end" => %{"character" => 29, "line" => 4}, "start" => %{"character" => 16, "line" => 4} } } @@ -800,7 +800,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do kind: 12, name: "def size(data)", range: %{ - "end" => %{"character" => 2, "line" => 2}, + "end" => %{"character" => 16, "line" => 2}, "start" => %{"character" => 2, "line" => 2} }, selectionRange: %{ @@ -816,7 +816,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do "start" => %{"character" => 0, "line" => 0} }, selectionRange: %{ - "end" => %{"character" => 12, "line" => 0}, + "end" => %{"character" => 22, "line" => 0}, "start" => %{"character" => 12, "line" => 0} } }, @@ -827,7 +827,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do kind: 12, name: "def size(binary)", range: %{ - "end" => %{"character" => 2, "line" => 6}, + "end" => %{"character" => 18, "line" => 6}, "start" => %{"character" => 2, "line" => 6} }, selectionRange: %{ @@ -854,7 +854,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do kind: 12, name: "def size(param)", range: %{ - "end" => %{"character" => 2, "line" => 10}, + "end" => %{"character" => 17, "line" => 10}, "start" => %{"character" => 2, "line" => 10} }, selectionRange: %{ @@ -1188,7 +1188,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do "start" => %{"character" => 2, "line" => 1} }, selectionRange: %{ - "end" => %{"character" => 8, "line" => 1}, + "end" => %{"character" => 17, "line" => 1}, "start" => %{"character" => 8, "line" => 1} } }, @@ -1879,15 +1879,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do } ], kind: 2, - name: "MyModuleTest", - range: %{ - "end" => %{"character" => 9, "line" => 4}, - "start" => %{"character" => 6, "line" => 1} - }, - selectionRange: %{ - "end" => %{"character" => 16, "line" => 1}, - "start" => %{"character" => 16, "line" => 1} - } + name: "MyModuleTest" } ]} = DocumentSymbols.symbols(uri, text, true) end @@ -1971,15 +1963,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do } ], kind: 2, - name: "MyModuleTest", - range: %{ - "end" => %{"character" => 9, "line" => 6}, - "start" => %{"character" => 6, "line" => 1} - }, - selectionRange: %{ - "end" => %{"character" => 16, "line" => 1}, - "start" => %{"character" => 16, "line" => 1} - } + name: "MyModuleTest" } ]} = DocumentSymbols.symbols(uri, text, true) end @@ -2028,15 +2012,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do } ], kind: 2, - name: "MyModuleTest", - range: %{ - "end" => %{"character" => 9, "line" => 6}, - "start" => %{"character" => 6, "line" => 1} - }, - selectionRange: %{ - "end" => %{"character" => 16, "line" => 1}, - "start" => %{"character" => 16, "line" => 1} - } + name: "MyModuleTest" } ]} = DocumentSymbols.symbols(uri, text, true) @@ -2202,15 +2178,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do } ], kind: 2, - name: "MyModuleTest", - range: %{ - "end" => %{"character" => 3, "line" => 9}, - "start" => %{"character" => 0, "line" => 0} - }, - selectionRange: %{ - "end" => %{"character" => 10, "line" => 0}, - "start" => %{"character" => 10, "line" => 0} - } + name: "MyModuleTest" } ]} = DocumentSymbols.symbols(uri, text, true) end From 41ad1f755e3d0929bd3667ec75507a7b1a21ada5 Mon Sep 17 00:00:00 2001 From: Dalibor Horinek Date: Tue, 21 Jun 2022 21:43:58 +0200 Subject: [PATCH 035/125] Add support for mix format plugins (#660) (#690) * Use formatter_for_file instead of formatter_opts_for_file which is deprecated now * Fallback to original Code.format_string! Co-authored-by: Dalibor Horinek --- .../providers/execute_command/apply_spec.ex | 5 ++-- .../language_server/providers/formatting.ex | 24 ++++++++++++++++--- .../lib/language_server/server.ex | 5 ++-- .../lib/language_server/source_file.ex | 17 ++++++------- 4 files changed, 36 insertions(+), 15 deletions(-) diff --git a/apps/language_server/lib/language_server/providers/execute_command/apply_spec.ex b/apps/language_server/lib/language_server/providers/execute_command/apply_spec.ex index 12b8d34fc..0eb67c090 100644 --- a/apps/language_server/lib/language_server/providers/execute_command/apply_spec.ex +++ b/apps/language_server/lib/language_server/providers/execute_command/apply_spec.ex @@ -56,8 +56,8 @@ defmodule ElixirLS.LanguageServer.Providers.ExecuteCommand.ApplySpec do formatted = try do target_line_length = - case SourceFile.formatter_opts(uri) do - {:ok, opts} -> Keyword.get(opts, :line_length, @default_target_line_length) + case SourceFile.formatter_for(uri) do + {:ok, {_, opts}} -> Keyword.get(opts, :line_length, @default_target_line_length) :error -> @default_target_line_length end @@ -94,3 +94,4 @@ defmodule ElixirLS.LanguageServer.Providers.ExecuteCommand.ApplySpec do end end end + diff --git a/apps/language_server/lib/language_server/providers/formatting.ex b/apps/language_server/lib/language_server/providers/formatting.ex index 12b413d94..f218ffcfe 100644 --- a/apps/language_server/lib/language_server/providers/formatting.ex +++ b/apps/language_server/lib/language_server/providers/formatting.ex @@ -5,7 +5,14 @@ defmodule ElixirLS.LanguageServer.Providers.Formatting do def format(%SourceFile{} = source_file, uri, project_dir) when is_binary(project_dir) do if can_format?(uri, project_dir) do - case SourceFile.formatter_opts(uri) do + case SourceFile.formatter_for(uri) do + {:ok, {formatter, opts}} -> + if should_format?(uri, project_dir, opts[:inputs]) do + do_format(source_file, formatter, opts) + else + {:ok, []} + end + {:ok, opts} -> if should_format?(uri, project_dir, opts[:inputs]) do do_format(source_file, opts) @@ -29,8 +36,11 @@ defmodule ElixirLS.LanguageServer.Providers.Formatting do do_format(source_file) end - defp do_format(%SourceFile{text: text}, opts \\ []) do - formatted = IO.iodata_to_binary([Code.format_string!(text, opts), ?\n]) + + defp do_format(%SourceFile{} = source_file, opts \\ []), do: do_format(source_file, nil, opts) + + defp do_format(%SourceFile{text: text}, formatter, opts) do + formatted = get_formatted(text, formatter, opts) response = text @@ -43,6 +53,14 @@ defmodule ElixirLS.LanguageServer.Providers.Formatting do {:error, :internal_error, "Unable to format due to syntax error"} end + defp get_formatted(text, formatter, _) when is_function(formatter) do + formatter.(text) + end + + defp get_formatted(text, _, opts) do + IO.iodata_to_binary([Code.format_string!(text, opts), ?\n]) + end + # If in an umbrella project, the cwd might be set to a sub-app if it's being compiled. This is # fine if the file we're trying to format is in that app. Otherwise, we return an error. defp can_format?(file_uri = "file:" <> _, project_dir) do diff --git a/apps/language_server/lib/language_server/server.ex b/apps/language_server/lib/language_server/server.ex index 9cfa998a6..b46d43708 100644 --- a/apps/language_server/lib/language_server/server.ex +++ b/apps/language_server/lib/language_server/server.ex @@ -674,8 +674,8 @@ defmodule ElixirLS.LanguageServer.Server do !!get_in(state.client_capabilities, ["textDocument", "signatureHelp"]) locals_without_parens = - case SourceFile.formatter_opts(uri) do - {:ok, opts} -> Keyword.get(opts, :locals_without_parens, []) + case SourceFile.formatter_for(uri) do + {:ok, {_, opts}} -> Keyword.get(opts, :locals_without_parens, []) :error -> [] end |> MapSet.new() @@ -1278,3 +1278,4 @@ defmodule ElixirLS.LanguageServer.Server do end) end end + diff --git a/apps/language_server/lib/language_server/source_file.ex b/apps/language_server/lib/language_server/source_file.ex index 57cbe02a7..11a4029cf 100644 --- a/apps/language_server/lib/language_server/source_file.ex +++ b/apps/language_server/lib/language_server/source_file.ex @@ -334,16 +334,16 @@ defmodule ElixirLS.LanguageServer.SourceFile do """ end - @spec formatter_opts(String.t()) :: {:ok, keyword()} | :error - def formatter_opts(uri = "file:" <> _) do + @spec formatter_for(String.t()) :: {:ok, keyword()} | :error + def formatter_for(uri = "file:" <> _) do path = path_from_uri(uri) try do - opts = - path - |> Mix.Tasks.Format.formatter_opts_for_file() - - {:ok, opts} + if function_exported?(Mix.Tasks.Format, :formatter_for_file, 1) do + {:ok, Mix.Tasks.Format.formatter_for_file(path)} + else + {:ok, {nil, Mix.Tasks.Format.formatter_opts_for_file(path)}} + end rescue e -> IO.warn( @@ -354,7 +354,7 @@ defmodule ElixirLS.LanguageServer.SourceFile do end end - def formatter_opts(_), do: :error + def formatter_for(_), do: :error defp format_code(code, opts) do try do @@ -431,3 +431,4 @@ defmodule ElixirLS.LanguageServer.SourceFile do {elixir_line - 1, utf16_character} end end + From 5ef73a5e40a3ca681bdc6a9e050269aed2c322cc Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sun, 26 Jun 2022 10:54:11 +0200 Subject: [PATCH 036/125] return info about pids, ports and funs in debugger evaluator --- .../lib/debugger/variables.ex | 56 +++++++++++++++++++ .../test/variables_test.exs | 27 ++++++++- 2 files changed, 80 insertions(+), 3 deletions(-) diff --git a/apps/elixir_ls_debugger/lib/debugger/variables.ex b/apps/elixir_ls_debugger/lib/debugger/variables.ex index 360f93e80..903833684 100644 --- a/apps/elixir_ls_debugger/lib/debugger/variables.ex +++ b/apps/elixir_ls_debugger/lib/debugger/variables.ex @@ -16,6 +16,22 @@ defmodule ElixirLS.Debugger.Variables do end end + def child_type(var) when is_function(var), do: :named + + def child_type(var) when is_pid(var) do + case :erlang.process_info(var) do + :undefined -> :indexed + results -> :named + end + end + + def child_type(var) when is_port(var) do + case :erlang.port_info(var) do + :undefined -> :indexed + results -> :named + end + end + def child_type(_var), do: nil def children(var, start, count) when is_list(var) do @@ -64,6 +80,27 @@ defmodule ElixirLS.Debugger.Variables do end end + def children(var, start, count) when is_function(var) do + :erlang.fun_info(var) + |> children(start, count) + end + + def children(var, start, count) when is_pid(var) do + case :erlang.process_info(var) do + :undefined -> ["process is not alive"] + results -> results + end + |> children(start, count) + end + + def children(var, start, count) when is_port(var) do + case :erlang.port_info(var) do + :undefined -> ["port is not open"] + results -> results + end + |> children(start, count) + end + def children(_var, _start, _count) do [] end @@ -84,6 +121,25 @@ defmodule ElixirLS.Debugger.Variables do map_size(var) end + def num_children(var) when is_function(var) do + :erlang.fun_info(var) + |> Enum.count() + end + + def num_children(var) when is_pid(var) do + case :erlang.process_info(var) do + :undefined -> 1 + results -> results |> Enum.count() + end + end + + def num_children(var) when is_port(var) do + case :erlang.port_info(var) do + :undefined -> 1 + results -> results |> Enum.count() + end + end + def num_children(_var) do 0 end diff --git a/apps/elixir_ls_debugger/test/variables_test.exs b/apps/elixir_ls_debugger/test/variables_test.exs index 27b385b7c..82d7150cf 100644 --- a/apps/elixir_ls_debugger/test/variables_test.exs +++ b/apps/elixir_ls_debugger/test/variables_test.exs @@ -79,11 +79,14 @@ defmodule ElixirLS.Debugger.VariablesTest do assert Variables.num_children(:erlang.make_ref()) == 0 - assert Variables.num_children(fn -> :ok end) == 0 + # As of OTP 24 10 values but it's better not to hardcode that + assert Variables.num_children(fn -> :ok end) != 0 - assert Variables.num_children(spawn(fn -> :ok end)) == 0 + # As of OTP 24 16 values but it's better not to hardcode that + assert Variables.num_children(self()) != 0 - assert Variables.num_children(hd(:erlang.ports())) == 0 + # As of OTP 24 7 values but it's better not to hardcode that + assert Variables.num_children(hd(:erlang.ports())) != 0 assert Variables.num_children([]) == 0 assert Variables.num_children([1]) == 1 @@ -173,5 +176,23 @@ defmodule ElixirLS.Debugger.VariablesTest do assert Variables.children(<<0::size(17)>>, 1, 10) == [{"1", 0}, {"2", <<0::size(1)>>}] assert Variables.children(<<0::size(17)>>, 1, 1) == [{"1", 0}] end + + test "fun" do + children = Variables.children(fn -> :ok end, 0, 10) + assert children[:module] == ElixirLS.Debugger.VariablesTest + assert children[:type] == :local + assert children[:arity] == 0 + end + + test "pid" do + children = Variables.children(self(), 0, 10) + assert children[:trap_exit] == false + assert children[:status] == :running + end + + test "port" do + children = Variables.children(hd(:erlang.ports()), 0, 10) + assert children[:name] == 'forker' + end end end From ea543d4630cedb00d8c2a17c784a5ba1f8d83850 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sun, 26 Jun 2022 11:56:00 +0200 Subject: [PATCH 037/125] return detail in debugger completions Added in DAP 1.53 --- apps/elixir_ls_debugger/lib/debugger/completions.ex | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/apps/elixir_ls_debugger/lib/debugger/completions.ex b/apps/elixir_ls_debugger/lib/debugger/completions.ex index 17be5216a..45e8d1ed0 100644 --- a/apps/elixir_ls_debugger/lib/debugger/completions.ex +++ b/apps/elixir_ls_debugger/lib/debugger/completions.ex @@ -12,6 +12,7 @@ defmodule ElixirLS.Debugger.Completions do when type in [:function, :macro] do %{ type: "function", + detail: Atom.to_string(type), label: "#{name}/#{arity}", text: snippet || name } @@ -19,6 +20,7 @@ defmodule ElixirLS.Debugger.Completions do def map(%{ type: :module, + subtype: subtype, name: name }) do text = @@ -29,6 +31,7 @@ defmodule ElixirLS.Debugger.Completions do %{ type: "module", + detail: if(subtype != nil, do: Atom.to_string(subtype)), label: name, text: text } @@ -46,10 +49,18 @@ defmodule ElixirLS.Debugger.Completions do def map(%{ type: :field, + subtype: subtype, name: name }) do + detail = + case subtype do + :struct_field -> "struct field" + :map_key -> "map key" + end + %{ type: "field", + detail: detail, label: name } end From 026b10aa4dc7048a5ca93250a432a823b3e48d6f Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sun, 26 Jun 2022 13:32:58 +0200 Subject: [PATCH 038/125] bump elixir_sense drop support for elixir 1.10 --- .github/workflows/ci.yml | 2 +- .github/workflows/release-asset.yml | 2 +- .release-tool-versions | 2 +- CHANGELOG.md | 3 +++ README.md | 3 +-- apps/elixir_ls_debugger/lib/debugger/cli.ex | 13 ------------- apps/elixir_ls_debugger/mix.exs | 4 ++-- apps/elixir_ls_debugger/test/test_helper.exs | 4 ---- apps/elixir_ls_utils/lib/minimum_version.ex | 4 ++-- apps/elixir_ls_utils/mix.exs | 4 ++-- .../elixir_ls_utils/test/support/mix_test.case.ex | 15 ++------------- apps/elixir_ls_utils/test/test_helper.exs | 4 ---- .../lib/language_server/diagnostics.ex | 2 -- apps/language_server/mix.exs | 4 ++-- apps/language_server/test/test_helper.exs | 4 ---- mix.exs | 2 +- mix.lock | 2 +- 17 files changed, 19 insertions(+), 55 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 52b4fb3e4..8865baa46 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,7 +16,7 @@ jobs: fail-fast: false matrix: include: - - elixir: 1.10.x + - elixir: 1.11.x otp: 22.3.4.x tests_may_fail: false check_unused_deps: true diff --git a/.github/workflows/release-asset.yml b/.github/workflows/release-asset.yml index 57ca25070..90acd424e 100644 --- a/.github/workflows/release-asset.yml +++ b/.github/workflows/release-asset.yml @@ -38,7 +38,7 @@ jobs: otp-version: '24.1' - elixir-version: '1.11' otp-version: '23.3' - - elixir-version: '1.10' + - elixir-version: '1.11' otp-version: '22.3' default: true diff --git a/.release-tool-versions b/.release-tool-versions index 8ee835a58..b87c5fe43 100644 --- a/.release-tool-versions +++ b/.release-tool-versions @@ -3,5 +3,5 @@ # # The versions selected here are the versions that are used to build a binary # release for distribution -elixir 1.10.4-otp-22 +elixir 1.11.4-otp-22 erlang 22.3.4.20 diff --git a/CHANGELOG.md b/CHANGELOG.md index fedd9a36d..4edf36fc7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ ### Unreleased +**Deprecations** +- Minimum version of Elixir is now 1.11 + ### v0.10.0: 10 June 2022 Improvements to debugger addapter: diff --git a/README.md b/README.md index 16f11e466..9030dac08 100644 --- a/README.md +++ b/README.md @@ -87,7 +87,7 @@ For VSCode install the extension: https://marketplace.visualstudio.com/items?ite Elixir: -- 1.10.0 minimum +- 1.11.0 minimum Erlang: @@ -284,7 +284,6 @@ https://github.com/elixir-lsp/elixir-ls/issues/364#issuecomment-829589139 * `.exs` files don't return compilation errors * "Fetching n dependencies" sometimes get stuck (remove the `.elixir_ls` directory to fix) -* Debugger doesn't work in Elixir 1.10.0 - 1.10.2 (but it should work in 1.10.3 when [this fix](https://github.com/elixir-lang/elixir/pull/9864) is released) * "Go to definition" does not work within the `scope` of a Phoenix router * On first launch dialyzer will cause high CPU usage for a considerable time * Dialyzer does not pick up changes involving remote types (https://github.com/elixir-lsp/elixir-ls/issues/502) diff --git a/apps/elixir_ls_debugger/lib/debugger/cli.ex b/apps/elixir_ls_debugger/lib/debugger/cli.ex index b597191bd..bf019f9a5 100644 --- a/apps/elixir_ls_debugger/lib/debugger/cli.ex +++ b/apps/elixir_ls_debugger/lib/debugger/cli.ex @@ -22,18 +22,5 @@ defmodule ElixirLS.Debugger.CLI do with {:error, message} <- ElixirLS.Utils.MinimumVersion.check_otp_version() do Output.print_err("WARNING: " <> message) end - - # Debugging does not work on Elixir 1.10.0-1.10.2: - # https://github.com/elixir-lsp/elixir-ls/issues/158 - elixir_version = System.version() - - if Version.match?(elixir_version, ">= 1.10.0") && Version.match?(elixir_version, "< 1.10.3") do - message = - "WARNING: Debugging is not supported on Elixir #{elixir_version}. Please upgrade" <> - " to at least 1.10.3\n" <> - "more info: https://github.com/elixir-lsp/elixir-ls/issues/158" - - Output.print_err(message) - end end end diff --git a/apps/elixir_ls_debugger/mix.exs b/apps/elixir_ls_debugger/mix.exs index c35adb135..83bfba583 100644 --- a/apps/elixir_ls_debugger/mix.exs +++ b/apps/elixir_ls_debugger/mix.exs @@ -4,12 +4,12 @@ defmodule ElixirLS.Debugger.Mixfile do def project do [ app: :elixir_ls_debugger, - version: "0.10.0", + version: "0.11.0", build_path: "../../_build", config_path: "../../config/config.exs", deps_path: "../../deps", lockfile: "../../mix.lock", - elixir: ">= 1.10.0", + elixir: ">= 1.11.0", build_embedded: false, start_permanent: true, build_per_environment: false, diff --git a/apps/elixir_ls_debugger/test/test_helper.exs b/apps/elixir_ls_debugger/test/test_helper.exs index eb3261d41..1650a4495 100644 --- a/apps/elixir_ls_debugger/test/test_helper.exs +++ b/apps/elixir_ls_debugger/test/test_helper.exs @@ -1,6 +1,2 @@ -if Version.match?(System.version(), ">= 1.11.0") do - Code.put_compiler_option(:warnings_as_errors, true) -end - Application.put_env(:elixir_ls_debugger, :test_mode, true) ExUnit.start(exclude: [pending: true]) diff --git a/apps/elixir_ls_utils/lib/minimum_version.ex b/apps/elixir_ls_utils/lib/minimum_version.ex index e3fab9786..0d7421339 100644 --- a/apps/elixir_ls_utils/lib/minimum_version.ex +++ b/apps/elixir_ls_utils/lib/minimum_version.ex @@ -11,11 +11,11 @@ defmodule ElixirLS.Utils.MinimumVersion do end def check_elixir_version do - if Version.match?(System.version(), ">= 1.10.0") do + if Version.match?(System.version(), ">= 1.11.0") do :ok else {:error, - "Elixir versions below 1.10 are not supported. (Currently running v#{System.version()})"} + "Elixir versions below 1.11 are not supported. (Currently running v#{System.version()})"} end end end diff --git a/apps/elixir_ls_utils/mix.exs b/apps/elixir_ls_utils/mix.exs index cc3487407..b00cb2fa4 100644 --- a/apps/elixir_ls_utils/mix.exs +++ b/apps/elixir_ls_utils/mix.exs @@ -4,13 +4,13 @@ defmodule ElixirLS.Utils.Mixfile do def project do [ app: :elixir_ls_utils, - version: "0.10.0", + version: "0.11.0", build_path: "../../_build", config_path: "../../config/config.exs", deps_path: "../../deps", elixirc_paths: elixirc_paths(Mix.env()), lockfile: "../../mix.lock", - elixir: ">= 1.10.0", + elixir: ">= 1.11.0", build_embedded: false, start_permanent: false, build_per_environment: false, diff --git a/apps/elixir_ls_utils/test/support/mix_test.case.ex b/apps/elixir_ls_utils/test/support/mix_test.case.ex index 116f787ca..b0a8fa622 100644 --- a/apps/elixir_ls_utils/test/support/mix_test.case.ex +++ b/apps/elixir_ls_utils/test/support/mix_test.case.ex @@ -129,7 +129,8 @@ defmodule ElixirLS.Utils.MixTest.Case do defp restore_project_stack!(stack) do # FIXME: Private API Mix.ProjectStack.clear_stack() - clear_mix_cache() + # FIXME: Private API + Mix.State.clear_cache() for %{name: module, file: file} <- stack do :code.purge(module) @@ -141,18 +142,6 @@ defmodule ElixirLS.Utils.MixTest.Case do end end - # FIXME: Private API - defp clear_mix_cache do - module = - if Version.match?(System.version(), ">= 1.10.0") do - Mix.State - else - Mix.ProjectStack - end - - module.clear_cache() - end - def capture_log_and_io(device, fun) when is_function(fun, 0) do # Logger gets stopped during some tests so restart it to be able to capture logs (and kept the # test output clean) diff --git a/apps/elixir_ls_utils/test/test_helper.exs b/apps/elixir_ls_utils/test/test_helper.exs index c8a7de607..3c96d9e7a 100644 --- a/apps/elixir_ls_utils/test/test_helper.exs +++ b/apps/elixir_ls_utils/test/test_helper.exs @@ -1,5 +1 @@ -if Version.match?(System.version(), ">= 1.11.0") do - Code.put_compiler_option(:warnings_as_errors, true) -end - ExUnit.start(exclude: [pending: true]) diff --git a/apps/language_server/lib/language_server/diagnostics.ex b/apps/language_server/lib/language_server/diagnostics.ex index 9b8867d2e..6f2c3143f 100644 --- a/apps/language_server/lib/language_server/diagnostics.ex +++ b/apps/language_server/lib/language_server/diagnostics.ex @@ -117,9 +117,7 @@ defmodule ElixirLS.LanguageServer.Diagnostics do end defp get_message_parts(message) do - # since elixir 1.11 eex compiler returns line and column on error case Regex.run(~r/^(.*?):(\d+)(:(\d+))?: (.*)/s, message) do - [_, file, line, description] -> {file, line, 0, description} [_, file, line, _, column, description] -> {file, line, column, description} _ -> nil end diff --git a/apps/language_server/mix.exs b/apps/language_server/mix.exs index 6daed43d3..0bc811424 100644 --- a/apps/language_server/mix.exs +++ b/apps/language_server/mix.exs @@ -4,8 +4,8 @@ defmodule ElixirLS.LanguageServer.Mixfile do def project do [ app: :language_server, - version: "0.10.0", - elixir: ">= 1.10.0", + version: "0.11.0", + elixir: ">= 1.11.0", build_path: "../../_build", config_path: "../../config/config.exs", deps_path: "../../deps", diff --git a/apps/language_server/test/test_helper.exs b/apps/language_server/test/test_helper.exs index 5f20b536b..dd9854a0a 100644 --- a/apps/language_server/test/test_helper.exs +++ b/apps/language_server/test/test_helper.exs @@ -1,7 +1,3 @@ -if Version.match?(System.version(), ">= 1.11.0") do - Code.put_compiler_option(:warnings_as_errors, true) -end - Application.put_env(:language_server, :test_mode, true) Application.ensure_started(:stream_data) ExUnit.start(exclude: [pending: true]) diff --git a/mix.exs b/mix.exs index 72cdbe02e..a2654f81e 100644 --- a/mix.exs +++ b/mix.exs @@ -9,7 +9,7 @@ defmodule ElixirLS.Mixfile do start_permanent: Mix.env() == :prod, build_per_environment: false, deps: deps(), - elixir: ">= 1.10.0", + elixir: ">= 1.11.0", dialyzer: [ plt_add_apps: [:dialyxir, :debugger, :dialyzer, :hipe], flags: [ diff --git a/mix.lock b/mix.lock index acc082fc4..86b17d3cf 100644 --- a/mix.lock +++ b/mix.lock @@ -1,7 +1,7 @@ %{ "dialyxir": {:hex, :dialyxir, "1.1.0", "c5aab0d6e71e5522e77beff7ba9e08f8e02bad90dfbeffae60eaf0cb47e29488", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "07ea8e49c45f15264ebe6d5b93799d4dd56a44036cf42d0ad9c960bc266c0b9a"}, "docsh": {:hex, :docsh, "0.7.2", "f893d5317a0e14269dd7fe79cf95fb6b9ba23513da0480ec6e77c73221cae4f2", [:rebar3], [{:providers, "1.8.1", [hex: :providers, repo: "hexpm", optional: false]}], "hexpm", "4e7db461bb07540d2bc3d366b8513f0197712d0495bb85744f367d3815076134"}, - "elixir_sense": {:git, "https://github.com/elixir-lsp/elixir_sense.git", "8a97e2b8f8cadef839da56c6b7e9bef91d332ce2", []}, + "elixir_sense": {:git, "https://github.com/elixir-lsp/elixir_sense.git", "192f5a18f18f2d86ff5731af143ea628c00c01c7", []}, "erl2ex": {:git, "https://github.com/dazuma/erl2ex.git", "244c2d9ed5805ef4855a491d8616b8842fef7ca4", []}, "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, "getopt": {:hex, :getopt, "1.0.1", "c73a9fa687b217f2ff79f68a3b637711bb1936e712b521d8ce466b29cbf7808a", [:rebar3], [], "hexpm", "53e1ab83b9ceb65c9672d3e7a35b8092e9bdc9b3ee80721471a161c10c59959c"}, From 78c95160bd4c840df9e884271f5736ebb41a7f8c Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sun, 26 Jun 2022 13:42:01 +0200 Subject: [PATCH 039/125] missing commit --- apps/elixir_ls_utils/test/support/mix_test.case.ex | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/elixir_ls_utils/test/support/mix_test.case.ex b/apps/elixir_ls_utils/test/support/mix_test.case.ex index b0a8fa622..8eb9ed28f 100644 --- a/apps/elixir_ls_utils/test/support/mix_test.case.ex +++ b/apps/elixir_ls_utils/test/support/mix_test.case.ex @@ -101,7 +101,8 @@ defmodule ElixirLS.Utils.MixTest.Case do defp clear_project_stack! do stack = clear_project_stack!([]) - clear_mix_cache() + # FIXME: Private API + Mix.State.clear_cache() # Attempt to purge mixfiles for dependencies to avoid module redefinition warnings mix_exs = MixfileHelpers.mix_exs() From 42083c99ffa89acedd4de885b8742882789832e0 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Mon, 27 Jun 2022 07:58:44 +0200 Subject: [PATCH 040/125] stop hex app in test fixture Fixes #627 --- apps/elixir_ls_utils/test/support/mix_test.case.ex | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/elixir_ls_utils/test/support/mix_test.case.ex b/apps/elixir_ls_utils/test/support/mix_test.case.ex index 8eb9ed28f..3226669e2 100644 --- a/apps/elixir_ls_utils/test/support/mix_test.case.ex +++ b/apps/elixir_ls_utils/test/support/mix_test.case.ex @@ -75,7 +75,11 @@ defmodule ElixirLS.Utils.MixTest.Case do previous = :code.all_loaded() project_stack = clear_project_stack!() - ExUnit.CaptureLog.capture_log(fn -> Application.stop(:mix) end) + ExUnit.CaptureLog.capture_log(fn -> + Application.stop(:mix) + Application.stop(:hex) + end) + Application.start(:mix) try do From 911688b618d2180dcb722db5bbbad220f4b7c062 Mon Sep 17 00:00:00 2001 From: Rafael Madriz Date: Mon, 27 Jun 2022 04:04:04 -0300 Subject: [PATCH 041/125] Add `nvim-lspconfig` to list of IDEs plugins (#706) --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 9030dac08..7c6f1f3e8 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,7 @@ Note: On first run Dialyzer will build a PLT cache which will take a considerabl | Kate | [built-in LSP Client plugin](https://kate-editor.org/post/2020/2020-01-01-kate-lsp-client-status/) | Does not support debugger | | Neovim | [coc.nvim](https://github.com/neoclide/coc.nvim) | Does not support debugger | | Neovim | [nvim-dap](https://github.com/mfussenegger/nvim-dap) | Supports debugger only | +| Neovim | [nvim-lspconfig](https://github.com/neovim/nvim-lspconfig) | Does not support debugger | | Nova | [nova-elixir-ls](https://github.com/raulchedrese/nova-elixir-ls) | | | Sublime Text | [LSP-elixir](https://github.com/sublimelsp/LSP-elixir) | Does not support debugger | | Vim/Neovim | [ALE](https://github.com/w0rp/ale) | Does not support debugger or @spec suggestions | From 5fe75badaac26297afe6d9a9610f08e07cc40df7 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Wed, 29 Jun 2022 23:34:36 +0200 Subject: [PATCH 042/125] Guard against invalid positions in diagnostics Return line with column from message Fixes #707 --- .../lib/language_server/diagnostics.ex | 53 ++++++++++--------- .../lib/language_server/dialyzer.ex | 23 ++++++-- .../providers/execute_command/apply_spec.ex | 1 - .../language_server/providers/formatting.ex | 1 - .../lib/language_server/server.ex | 1 - .../lib/language_server/source_file.ex | 1 - .../language_server/test/diagnostics_test.exs | 24 +++++++++ 7 files changed, 72 insertions(+), 32 deletions(-) diff --git a/apps/language_server/lib/language_server/diagnostics.ex b/apps/language_server/lib/language_server/diagnostics.ex index 6f2c3143f..f123b7a5d 100644 --- a/apps/language_server/lib/language_server/diagnostics.ex +++ b/apps/language_server/lib/language_server/diagnostics.ex @@ -4,13 +4,13 @@ defmodule ElixirLS.LanguageServer.Diagnostics do def normalize(diagnostics, root_path) do for diagnostic <- diagnostics do - {type, file, line, description, stacktrace} = + {type, file, position, description, stacktrace} = extract_message_info(diagnostic.message, root_path) diagnostic |> update_message(type, description, stacktrace) |> maybe_update_file(file) - |> maybe_update_position(type, line, stacktrace) + |> maybe_update_position(type, position, stacktrace) end end @@ -32,9 +32,9 @@ defmodule ElixirLS.LanguageServer.Diagnostics do stacktrace = reversed_stacktrace |> Enum.map(&String.trim/1) |> Enum.reverse() {type, message_without_type} = split_type_and_message(message) - {file, line, description} = split_file_and_description(message_without_type, root_path) + {file, position, description} = split_file_and_description(message_without_type, root_path) - {type, file, line, description, stacktrace} + {type, file, position, description, stacktrace} end defp update_message(diagnostic, type, description, stacktrace) do @@ -68,31 +68,31 @@ defmodule ElixirLS.LanguageServer.Diagnostics do end end - defp maybe_update_position(diagnostic, "TokenMissingError", line, stacktrace) do + defp maybe_update_position(diagnostic, "TokenMissingError", position, stacktrace) do case extract_line_from_missing_hint(diagnostic.message) do - line when is_integer(line) -> + line when is_integer(line) and line > 0 -> %{diagnostic | position: line} _ -> - do_maybe_update_position(diagnostic, line, stacktrace) + do_maybe_update_position(diagnostic, position, stacktrace) end end - defp maybe_update_position(diagnostic, _type, line, stacktrace) do - do_maybe_update_position(diagnostic, line, stacktrace) + defp maybe_update_position(diagnostic, _type, position, stacktrace) do + do_maybe_update_position(diagnostic, position, stacktrace) end - defp do_maybe_update_position(diagnostic, line, stacktrace) do + defp do_maybe_update_position(diagnostic, position, stacktrace) do cond do - line -> - %{diagnostic | position: line} + position != nil -> + %{diagnostic | position: position} diagnostic.position -> diagnostic true -> line = extract_line_from_stacktrace(diagnostic.file, stacktrace) - %{diagnostic | position: line} + %{diagnostic | position: max(line, 0)} end end @@ -107,9 +107,18 @@ defmodule ElixirLS.LanguageServer.Diagnostics do end defp split_file_and_description(message, root_path) do - with {file, line, _column, description} <- get_message_parts(message), + with {file, line, column, description} <- get_message_parts(message), {:ok, path} <- file_path(file, root_path) do - {path, String.to_integer(line), description} + line = String.to_integer(line) + + position = + cond do + line == 0 -> 0 + column == "" -> line + true -> {line, String.to_integer(column)} + end + + {path, position, description} else _ -> {nil, nil, message} @@ -279,7 +288,7 @@ defmodule ElixirLS.LanguageServer.Diagnostics do lines = SourceFile.lines(source_file) # line is 1 based start_line = Enum.at(lines, line_start - 1) - # SourceFile.elixir_character_to_lsp assumes char to be 1 based but it's 0 based bere + # SourceFile.elixir_character_to_lsp assumes char to be 1 based but it's 0 based here character = SourceFile.elixir_character_to_lsp(start_line, char_start + 1) %{ @@ -303,7 +312,7 @@ defmodule ElixirLS.LanguageServer.Diagnostics do start_line = Enum.at(lines, line_start - 1) end_line = Enum.at(lines, line_end - 1) - # SourceFile.elixir_character_to_lsp assumes char to be 1 based but it's 0 based bere + # SourceFile.elixir_character_to_lsp assumes char to be 1 based but it's 0 based here start_char = SourceFile.elixir_character_to_lsp(start_line, char_start + 1) end_char = SourceFile.elixir_character_to_lsp(end_line, char_end + 1) @@ -319,16 +328,10 @@ defmodule ElixirLS.LanguageServer.Diagnostics do } end - # position is 0 which means unknown - # we return the full file range - defp range(0, source_file) when not is_nil(source_file) do - SourceFile.full_range(source_file) - end - - # source file is unknown + # source file is unknown, position is 0 or invalid # we discard any position information as it is meaningless # unfortunately LSP does not allow `null` range so we need to return something - defp range(_, nil) do + defp range(_, _) do # we don't care about utf16 positions here as we send 0 %{"start" => %{"line" => 0, "character" => 0}, "end" => %{"line" => 0, "character" => 0}} end diff --git a/apps/language_server/lib/language_server/dialyzer.ex b/apps/language_server/lib/language_server/dialyzer.ex index b0e053583..48b9ffca3 100644 --- a/apps/language_server/lib/language_server/dialyzer.ex +++ b/apps/language_server/lib/language_server/dialyzer.ex @@ -475,18 +475,19 @@ defmodule ElixirLS.LanguageServer.Dialyzer do defp to_diagnostics(warnings_map, warn_opts, warning_format) do tags_enabled = Analyzer.matching_tags(warn_opts) + deps_path = Mix.Project.deps_path() for {_beam_file, warnings} <- warnings_map, - {source_file, line, data} <- warnings, + {source_file, position, data} <- warnings, {tag, _, _} = data, tag in tags_enabled, source_file = Path.absname(to_string(source_file)), in_project?(source_file), - not String.starts_with?(source_file, Mix.Project.deps_path()) do + not String.starts_with?(source_file, deps_path) do %Mix.Task.Compiler.Diagnostic{ compiler_name: "ElixirLS Dialyzer", file: source_file, - position: line, + position: normalize_postion(position), message: warning_message(data, warning_format), severity: :warning, details: data @@ -494,6 +495,22 @@ defmodule ElixirLS.LanguageServer.Dialyzer do end end + # up until OTP 23 position was line :: non_negative_integer + # starting from OTP 24 it is erl_anno:location() :: line | {line, column} + defp normalize_postion({line, column}) when line > 0 do + {line, column} + end + + # 0 means unknown line + defp normalize_postion(line) when line >= 0 do + line + end + + defp normalize_postion(position) do + IO.warn("dialyzer returned warning with invalid position #{inspect(position)}") + 0 + end + defp warning_message({_, _, {warning_name, args}} = raw_warning, warning_format) when warning_format in ["dialyxir_long", "dialyxir_short"] do format_function = diff --git a/apps/language_server/lib/language_server/providers/execute_command/apply_spec.ex b/apps/language_server/lib/language_server/providers/execute_command/apply_spec.ex index 0eb67c090..11be42cfe 100644 --- a/apps/language_server/lib/language_server/providers/execute_command/apply_spec.ex +++ b/apps/language_server/lib/language_server/providers/execute_command/apply_spec.ex @@ -94,4 +94,3 @@ defmodule ElixirLS.LanguageServer.Providers.ExecuteCommand.ApplySpec do end end end - diff --git a/apps/language_server/lib/language_server/providers/formatting.ex b/apps/language_server/lib/language_server/providers/formatting.ex index f218ffcfe..a0edd0a11 100644 --- a/apps/language_server/lib/language_server/providers/formatting.ex +++ b/apps/language_server/lib/language_server/providers/formatting.ex @@ -36,7 +36,6 @@ defmodule ElixirLS.LanguageServer.Providers.Formatting do do_format(source_file) end - defp do_format(%SourceFile{} = source_file, opts \\ []), do: do_format(source_file, nil, opts) defp do_format(%SourceFile{text: text}, formatter, opts) do diff --git a/apps/language_server/lib/language_server/server.ex b/apps/language_server/lib/language_server/server.ex index b46d43708..b66a2f4c7 100644 --- a/apps/language_server/lib/language_server/server.ex +++ b/apps/language_server/lib/language_server/server.ex @@ -1278,4 +1278,3 @@ defmodule ElixirLS.LanguageServer.Server do end) end end - diff --git a/apps/language_server/lib/language_server/source_file.ex b/apps/language_server/lib/language_server/source_file.ex index 11a4029cf..0db546307 100644 --- a/apps/language_server/lib/language_server/source_file.ex +++ b/apps/language_server/lib/language_server/source_file.ex @@ -431,4 +431,3 @@ defmodule ElixirLS.LanguageServer.SourceFile do {elixir_line - 1, utf16_character} end end - diff --git a/apps/language_server/test/diagnostics_test.exs b/apps/language_server/test/diagnostics_test.exs index d1f99bdec..999258a07 100644 --- a/apps/language_server/test/diagnostics_test.exs +++ b/apps/language_server/test/diagnostics_test.exs @@ -61,6 +61,30 @@ defmodule ElixirLS.LanguageServer.DiagnosticsTest do assert diagnostic.position == 3 end + test "update file and position with column if file is present in the message" do + root_path = Path.join(__DIR__, "fixtures/build_errors") + file = Path.join(root_path, "lib/has_error.ex") + position = 2 + + message = """ + ** (CompileError) lib/has_error.ex:3:5: some message + lib/my_app/my_module.ex:10: MyApp.MyModule.render/1 + """ + + [diagnostic | _] = + [build_diagnostic(message, file, position)] + |> Diagnostics.normalize(root_path) + + assert diagnostic.message == """ + (CompileError) some message + + Stacktrace: + │ lib/my_app/my_module.ex:10: MyApp.MyModule.render/1\ + """ + + assert diagnostic.position == {3, 5} + end + test "update file and position if file is present in the message (umbrella)" do root_path = Path.join(__DIR__, "fixtures/umbrella") file = Path.join(root_path, "lib/file_to_be_replaced.ex") From e4576def20c479dc7be7cc7e6079d017cb980c98 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Tue, 5 Jul 2022 08:07:22 +0200 Subject: [PATCH 043/125] preselect do completion to make it more preferred than defoverride Fixes #302 --- .../lib/language_server/providers/completion.ex | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/apps/language_server/lib/language_server/providers/completion.ex b/apps/language_server/lib/language_server/providers/completion.ex index 611a3508d..d6b2244f8 100644 --- a/apps/language_server/lib/language_server/providers/completion.ex +++ b/apps/language_server/lib/language_server/providers/completion.ex @@ -20,7 +20,8 @@ defmodule ElixirLS.LanguageServer.Providers.Completion do # Lower priority is shown higher in the result list :priority, :tags, - :command + :command, + {:preselect, false} ] @func_snippets %{ @@ -163,7 +164,9 @@ defmodule ElixirLS.LanguageServer.Providers.Completion do detail: "keyword", insert_text: "do\n $0\nend", tags: [], - priority: 0 + priority: 0, + # force selection over other longer not exact completions + preselect: true } [item | completion_items] @@ -978,9 +981,15 @@ defmodule ElixirLS.LanguageServer.Providers.Completion do insert_text_format(:snippet) else insert_text_format(:plain_text) - end + end, } + json = if item.preselect do + Map.put(json, "preselect", true) + else + json + end + # deprecated as of Language Server Protocol Specification - 3.15 json = if Keyword.get(options, :deprecated_supported, false) do From e0e682b8b609b9c51072d45a9879f932d02c072e Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Tue, 5 Jul 2022 10:28:50 +0200 Subject: [PATCH 044/125] Use chardata_to_string --- apps/language_server/lib/language_server/diagnostics.ex | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/apps/language_server/lib/language_server/diagnostics.ex b/apps/language_server/lib/language_server/diagnostics.ex index f123b7a5d..63ada28e2 100644 --- a/apps/language_server/lib/language_server/diagnostics.ex +++ b/apps/language_server/lib/language_server/diagnostics.ex @@ -14,15 +14,10 @@ defmodule ElixirLS.LanguageServer.Diagnostics do end end - defp extract_message_info(list, root_path) when is_list(list) do - list - |> Enum.join() - |> extract_message_info(root_path) - end - defp extract_message_info(diagnostic_message, root_path) do {reversed_stacktrace, reversed_description} = diagnostic_message + |> IO.chardata_to_string() |> String.trim_trailing() |> SourceFile.lines() |> Enum.reverse() From 68fe3f7be92836b7369035437e882f5e0604da6a Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sun, 10 Jul 2022 22:26:57 +0200 Subject: [PATCH 045/125] add github actions dependabot config --- .github/dependabot.yml | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..5ace4600a --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,6 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" From bdbfc5b638b998b67dfcc9d8eb657473663d4a11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Samson?= Date: Wed, 10 Aug 2022 19:04:20 +0200 Subject: [PATCH 046/125] Elixir 1.14 (#716) * bump elixir_sense * fix warnings * ensure Mix.Tasks.Format module is loaded * fix test * run formatter --- apps/elixir_ls_debugger/lib/debugger/variables.ex | 4 ++-- .../lib/language_server/providers/completion.ex | 13 +++++++------ .../lib/language_server/source_file.ex | 2 ++ .../test/providers/formatting_test.exs | 10 +++++++--- mix.lock | 2 +- 5 files changed, 19 insertions(+), 12 deletions(-) diff --git a/apps/elixir_ls_debugger/lib/debugger/variables.ex b/apps/elixir_ls_debugger/lib/debugger/variables.ex index 903833684..f86a4309a 100644 --- a/apps/elixir_ls_debugger/lib/debugger/variables.ex +++ b/apps/elixir_ls_debugger/lib/debugger/variables.ex @@ -21,14 +21,14 @@ defmodule ElixirLS.Debugger.Variables do def child_type(var) when is_pid(var) do case :erlang.process_info(var) do :undefined -> :indexed - results -> :named + _results -> :named end end def child_type(var) when is_port(var) do case :erlang.port_info(var) do :undefined -> :indexed - results -> :named + _results -> :named end end diff --git a/apps/language_server/lib/language_server/providers/completion.ex b/apps/language_server/lib/language_server/providers/completion.ex index d6b2244f8..7a3123461 100644 --- a/apps/language_server/lib/language_server/providers/completion.ex +++ b/apps/language_server/lib/language_server/providers/completion.ex @@ -981,14 +981,15 @@ defmodule ElixirLS.LanguageServer.Providers.Completion do insert_text_format(:snippet) else insert_text_format(:plain_text) - end, + end } - json = if item.preselect do - Map.put(json, "preselect", true) - else - json - end + json = + if item.preselect do + Map.put(json, "preselect", true) + else + json + end # deprecated as of Language Server Protocol Specification - 3.15 json = diff --git a/apps/language_server/lib/language_server/source_file.ex b/apps/language_server/lib/language_server/source_file.ex index 0db546307..0ded9737a 100644 --- a/apps/language_server/lib/language_server/source_file.ex +++ b/apps/language_server/lib/language_server/source_file.ex @@ -339,6 +339,8 @@ defmodule ElixirLS.LanguageServer.SourceFile do path = path_from_uri(uri) try do + true = Code.ensure_loaded?(Mix.Tasks.Format) + if function_exported?(Mix.Tasks.Format, :formatter_for_file, 1) do {:ok, Mix.Tasks.Format.formatter_for_file(path)} else diff --git a/apps/language_server/test/providers/formatting_test.exs b/apps/language_server/test/providers/formatting_test.exs index 778a1a504..30b833c53 100644 --- a/apps/language_server/test/providers/formatting_test.exs +++ b/apps/language_server/test/providers/formatting_test.exs @@ -500,7 +500,11 @@ defmodule ElixirLS.LanguageServer.Providers.FormattingTest do end def assert_formatted(path, project_dir) do - assert match?({:ok, [%{}]}, format(path, project_dir)), "expected '#{path}' to be formatted" + assert match?( + {:ok, [%ElixirLS.LanguageServer.Protocol.TextEdit{} | _]}, + format(path, project_dir) + ), + "expected '#{path}' to be formatted" end def refute_formatted(path, project_dir) do @@ -512,12 +516,12 @@ defmodule ElixirLS.LanguageServer.Providers.FormattingTest do path = maybe_convert_path_separators("#{project_dir}/#{path}") source_file = %SourceFile{ - text: "", + text: " asd = 1", version: 1, dirty?: true } - File.write!(path, "") + File.write!(path, " asd = 1") Formatting.format(source_file, SourceFile.path_to_uri(path), project_dir) end end diff --git a/mix.lock b/mix.lock index 86b17d3cf..30f8eac22 100644 --- a/mix.lock +++ b/mix.lock @@ -1,7 +1,7 @@ %{ "dialyxir": {:hex, :dialyxir, "1.1.0", "c5aab0d6e71e5522e77beff7ba9e08f8e02bad90dfbeffae60eaf0cb47e29488", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "07ea8e49c45f15264ebe6d5b93799d4dd56a44036cf42d0ad9c960bc266c0b9a"}, "docsh": {:hex, :docsh, "0.7.2", "f893d5317a0e14269dd7fe79cf95fb6b9ba23513da0480ec6e77c73221cae4f2", [:rebar3], [{:providers, "1.8.1", [hex: :providers, repo: "hexpm", optional: false]}], "hexpm", "4e7db461bb07540d2bc3d366b8513f0197712d0495bb85744f367d3815076134"}, - "elixir_sense": {:git, "https://github.com/elixir-lsp/elixir_sense.git", "192f5a18f18f2d86ff5731af143ea628c00c01c7", []}, + "elixir_sense": {:git, "https://github.com/elixir-lsp/elixir_sense.git", "08bac2a5a0ad867908d77a7087eac756bf7cce43", []}, "erl2ex": {:git, "https://github.com/dazuma/erl2ex.git", "244c2d9ed5805ef4855a491d8616b8842fef7ca4", []}, "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, "getopt": {:hex, :getopt, "1.0.1", "c73a9fa687b217f2ff79f68a3b637711bb1936e712b521d8ce466b29cbf7808a", [:rebar3], [], "hexpm", "53e1ab83b9ceb65c9672d3e7a35b8092e9bdc9b3ee80721471a161c10c59959c"}, From 9e83aa2d0ea3ac991af9ff8002c6a2a4922a0c2e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 13 Aug 2022 12:53:59 +0200 Subject: [PATCH 047/125] Bump actions/upload-artifact from 1 to 3 (#710) Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 1 to 3. - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/v1...v3) --- updated-dependencies: - dependency-name: actions/upload-artifact dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/docsite.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docsite.yml b/.github/workflows/docsite.yml index 075bd5cd2..0bdaaea4f 100644 --- a/.github/workflows/docsite.yml +++ b/.github/workflows/docsite.yml @@ -19,7 +19,7 @@ jobs: - name: Build run: mkdocs build -s - name: Upload artifact - uses: actions/upload-artifact@v1 + uses: actions/upload-artifact@v3 with: name: site path: site From 9aebbe8e9dbde72e722943ae2d12103f676d7eac Mon Sep 17 00:00:00 2001 From: Milo Lee Date: Sat, 13 Aug 2022 19:11:57 +0800 Subject: [PATCH 048/125] Fix import and alias module functions not showing view on hexdocs link on hover (#712) --- .../lib/language_server/providers/hover.ex | 4 +- .../test/providers/hover_test.exs | 44 +++++++++++++++++++ 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/apps/language_server/lib/language_server/providers/hover.ex b/apps/language_server/lib/language_server/providers/hover.ex index 7ebd1bf0e..041ed9095 100644 --- a/apps/language_server/lib/language_server/providers/hover.ex +++ b/apps/language_server/lib/language_server/providers/hover.ex @@ -25,11 +25,11 @@ defmodule ElixirLS.LanguageServer.Providers.Hover do %{subject: ""} -> nil - %{subject: subject, docs: docs} -> + %{subject: subject, docs: docs, actual_subject: actual_subject} -> line_text = Enum.at(SourceFile.lines(text), line - 1) range = highlight_range(line_text, line - 1, character - 1, subject) - %{"contents" => contents(docs, subject, project_dir), "range" => range} + %{"contents" => contents(docs, actual_subject, project_dir), "range" => range} end {:ok, response} diff --git a/apps/language_server/test/providers/hover_test.exs b/apps/language_server/test/providers/hover_test.exs index cc1aa967d..3399ed090 100644 --- a/apps/language_server/test/providers/hover_test.exs +++ b/apps/language_server/test/providers/hover_test.exs @@ -101,6 +101,50 @@ defmodule ElixirLS.LanguageServer.Providers.HoverTest do ) end + test "Import function hover" do + text = """ + defmodule MyModule do + import Task.Supervisor + + def hello() do + start_link() + end + end + """ + + {line, char} = {4, 5} + + assert {:ok, %{"contents" => %{kind: "markdown", value: v}}} = + Hover.hover(text, line, char, fake_dir()) + + assert String.starts_with?( + v, + "> Task.Supervisor.start_link(options \\\\\\\\ []) [view on hexdocs](https://hexdocs.pm/elixir/Task.Supervisor.html#start_link/1)" + ) + end + + test "Alias module function hover" do + text = """ + defmodule MyModule do + alias Task.Supervisor + + def hello() do + Supervisor.start_link() + end + end + """ + + {line, char} = {4, 15} + + assert {:ok, %{"contents" => %{kind: "markdown", value: v}}} = + Hover.hover(text, line, char, fake_dir()) + + assert String.starts_with?( + v, + "> Task.Supervisor.start_link(options \\\\\\\\ []) [view on hexdocs](https://hexdocs.pm/elixir/Task.Supervisor.html#start_link/1)" + ) + end + test "Erlang module hover is not support now" do text = """ defmodule MyModule do From 471cdd2c688d2659433a008c21ad450505dde406 Mon Sep 17 00:00:00 2001 From: Manos Emmanouilidis Date: Sat, 13 Aug 2022 14:14:29 +0300 Subject: [PATCH 049/125] Special case handling of Phoenix 'live' dir when suggesting module names (#705) When generating a live view with mix phx.gen.live the view and all related files are put under my_project_web/live/* Similar to other Phoenix paths (e.g. /controllers) this directory is not a part of the generated module names, and the suggested module names when auto-completing should account for that convention. --- .../lib/language_server/providers/completion.ex | 3 ++- apps/language_server/test/providers/completion_test.exs | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/language_server/lib/language_server/providers/completion.ex b/apps/language_server/lib/language_server/providers/completion.ex index 7a3123461..202441bc3 100644 --- a/apps/language_server/lib/language_server/providers/completion.ex +++ b/apps/language_server/lib/language_server/providers/completion.ex @@ -650,7 +650,8 @@ defmodule ElixirLS.LanguageServer.Providers.Completion do "channels", "plugs", "endpoints", - "sockets" + "sockets", + "live" ] do if String.ends_with?(project_web_dir, "_web") do # by convention Phoenix doesn't use these folders as part of the module names diff --git a/apps/language_server/test/providers/completion_test.exs b/apps/language_server/test/providers/completion_test.exs index 8ac722dc2..d4e1b01e2 100644 --- a/apps/language_server/test/providers/completion_test.exs +++ b/apps/language_server/test/providers/completion_test.exs @@ -1164,7 +1164,8 @@ defmodule ElixirLS.LanguageServer.Providers.CompletionTest do {"MyProjectWeb.MyView", "views/my_view.ex"}, {"MyProjectWeb.MyChannel", "channels/my_channel.ex"}, {"MyProjectWeb.MyEndpoint", "endpoints/my_endpoint.ex"}, - {"MyProjectWeb.MySocket", "sockets/my_socket.ex"} + {"MyProjectWeb.MySocket", "sockets/my_socket.ex"}, + {"MyProjectWeb.MyviewLive.MyComponent", "live/myview_live/my_component.ex"} ] |> Enum.each(fn {expected_module_name, partial_path} -> path = "some/path/my_project/lib/my_project_web/#{partial_path}" From 5e51da09da8b66cd005e051e9a4d42ae71b7cc59 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sun, 14 Aug 2022 09:01:38 +0200 Subject: [PATCH 050/125] Changelog updated --- CHANGELOG.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4edf36fc7..0b891c20a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,21 @@ ### Unreleased +### v0.11.0: 14 August 2022 + +Improvements: + +- Elixir 1.14 support +- Document symbols now return non empty selection ranges. This fixes breadcrumbs behavior in vscode +- Fixed dialyzer crash on OTP 25 +- Added support for mix formatter plugins ([Dalibor Horinek](https://github.com/DaliborHorinek)) +- Debugger now returns detailed info about ports, pids and function variables +- Debugger completions now return detal field +- Diagnostic positions now return column position returned by compiler (elixir 1.14+) +- Diagnostic position fixed to never return invalid negative values +- An exat `do` keyword completion is now preselected and more preferred over `defoverridable` +- Fixed hexdoc links in hover for aliased modules and imported functions ([Milo Lee](https://github.com/oo6)) +- Better module name suggestions in Phoenix `live` directory ([Manos Emmanouilidis](https://github.com/bottlenecked)) + **Deprecations** - Minimum version of Elixir is now 1.11 From fe1191031874bf0d05afa18d1886e8ac705fd94d Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sun, 14 Aug 2022 10:06:31 +0200 Subject: [PATCH 051/125] extend release asset matrix --- .github/workflows/release-asset.yml | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release-asset.yml b/.github/workflows/release-asset.yml index 90acd424e..d1bf789a8 100644 --- a/.github/workflows/release-asset.yml +++ b/.github/workflows/release-asset.yml @@ -33,9 +33,21 @@ jobs: matrix: include: - elixir-version: '1.13' - otp-version: '24.1' + otp-version: '25.0' + - elixir-version: '1.13' + otp-version: '24.3' + - elixir-version: '1.13' + otp-version: '23.3' + - elixir-version: '1.13' + otp-version: '22.3' - elixir-version: '1.12' - otp-version: '24.1' + otp-version: '24.3' + - elixir-version: '1.12' + otp-version: '23.3' + - elixir-version: '1.12' + otp-version: '22.3' + - elixir-version: '1.11' + otp-version: '24.3' - elixir-version: '1.11' otp-version: '23.3' - elixir-version: '1.11' @@ -74,7 +86,7 @@ jobs: with: upload_url: ${{ needs.release.outputs.upload_url }} asset_path: ./elixir-ls.zip - asset_name: elixir-ls-${{ matrix.elixir-version }}.zip + asset_name: elixir-ls-${{ matrix.elixir-version }}-${{ matrix.otp-version }}.zip asset_content_type: application/zip - name: Upload Default Release Asset From e3189637b202aeb5b616dad35666b259a72c51b2 Mon Sep 17 00:00:00 2001 From: Sahn Lam Date: Sat, 27 Aug 2022 01:54:17 -0700 Subject: [PATCH 052/125] A ~ is not expanded inside double quotes (#721) Use $HOME instead. --- apps/elixir_ls_utils/priv/launch.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/elixir_ls_utils/priv/launch.sh b/apps/elixir_ls_utils/priv/launch.sh index d715d752a..0e8aeb5f9 100755 --- a/apps/elixir_ls_utils/priv/launch.sh +++ b/apps/elixir_ls_utils/priv/launch.sh @@ -41,7 +41,7 @@ fi # give them the chance here. ELS_MODE will be set for # the really complex stuff. Use an XDG compliant path. -els_setup="${XDG_CONFIG_HOME:-~/.config}/elixir_ls/setup.sh" +els_setup="${XDG_CONFIG_HOME:-$HOME/.config}/elixir_ls/setup.sh" if test -f "${els_setup}" then . "${els_setup}" From caad581ceda32a0e7eeb30129b198e69d725cce0 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sat, 3 Sep 2022 07:51:44 +0200 Subject: [PATCH 053/125] add 1.14 to test matrix --- .github/workflows/ci.yml | 39 +++++++++++++++++++++++++++++---------- 1 file changed, 29 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8865baa46..71a793b22 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,31 +17,50 @@ jobs: matrix: include: - elixir: 1.11.x - otp: 22.3.4.x + otp: 22.3.x tests_may_fail: false - check_unused_deps: true + check_unused_deps: false - elixir: 1.11.x - otp: 23.3.4.x + otp: 23.3.x tests_may_fail: false - check_unused_deps: true + check_unused_deps: false - elixir: 1.12.x - otp: 23.3.4.x + otp: 23.3.x tests_may_fail: false - check_unused_deps: true + check_unused_deps: false - elixir: 1.13.x - otp: 22.3.4.x + otp: 22.3.x tests_may_fail: false check_unused_deps: true warnings_as_errors: false - elixir: 1.13.x - otp: 25.x + otp: 23.3.x tests_may_fail: false - check_unused_deps: true + check_unused_deps: false warnings_as_errors: false - elixir: 1.13.x otp: 24.3.x tests_may_fail: false - # Needs to be false until https://github.com/elixir-lsp/elixir-ls/issues/642 is fixed + check_unused_deps: false + warnings_as_errors: false + - elixir: 1.13.x + otp: 25.x + tests_may_fail: false + check_unused_deps: false + warnings_as_errors: false + - elixir: 1.14.x + otp: 23.3.x + tests_may_fail: false + check_unused_deps: false + warnings_as_errors: false + - elixir: 1.14.x + otp: 24.3.x + tests_may_fail: false + check_unused_deps: false + warnings_as_errors: false + - elixir: 1.14.x + otp: 25.x + tests_may_fail: false warnings_as_errors: false check_formatted: true check_unused_deps: true From 6c1833c5e8ff5e36fc7fef3424bb0fab5d8d5e1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Samson?= Date: Sat, 10 Sep 2022 20:01:15 +0200 Subject: [PATCH 054/125] remove load_all_applications (#725) since elixir 1.11 mix compile loads applications by default --- .../lib/language_server/build.ex | 31 ------------------- .../lib/language_server/server.ex | 12 ++----- .../test/providers/code_lens/test_test.exs | 2 -- .../test/providers/completion_test.exs | 2 -- 4 files changed, 3 insertions(+), 44 deletions(-) diff --git a/apps/language_server/lib/language_server/build.ex b/apps/language_server/lib/language_server/build.ex index df27e6d2d..4dec17f05 100644 --- a/apps/language_server/lib/language_server/build.ex +++ b/apps/language_server/lib/language_server/build.ex @@ -29,10 +29,6 @@ defmodule ElixirLS.LanguageServer.Build do purge_consolidated_protocols() {status, diagnostics} = compile() - if status in [:ok, :noop] and Keyword.get(opts, :load_all_mix_applications?) do - load_all_mix_applications() - end - diagnostics = Diagnostics.normalize(diagnostics, root_path) Server.build_finished(parent, {status, mixfile_diagnostics ++ diagnostics}) @@ -108,33 +104,6 @@ defmodule ElixirLS.LanguageServer.Build do end end - # TODO It looks like that function is no longer needed on elixir >= 1.11 - # it was added in https://github.com/elixir-lsp/elixir-ls/pull/227 - # removing it doesn't break tests and I'm not able to reproduce - # https://github.com/elixir-lsp/elixir-ls/issues/209 on recent elixir (1.13) - def load_all_mix_applications do - apps = - cond do - Mix.Project.umbrella?() -> - Mix.Project.apps_paths() |> Map.keys() - - app = Keyword.get(Mix.Project.config(), :app) -> - [app] - - true -> - [] - end - - Enum.each(apps, fn app -> - true = Code.prepend_path(Path.join(Mix.Project.build_path(), "lib/#{app}/ebin")) - - case Application.load(app) do - :ok -> :ok - {:error, {:already_loaded, _}} -> :ok - end - end) - end - defp compile do case Mix.Task.run("compile", ["--return-errors", "--ignore-module-conflict"]) do {status, diagnostics} when status in [:ok, :error, :noop] and is_list(diagnostics) -> diff --git a/apps/language_server/lib/language_server/server.ex b/apps/language_server/lib/language_server/server.ex index b66a2f4c7..37abaddac 100644 --- a/apps/language_server/lib/language_server/server.ex +++ b/apps/language_server/lib/language_server/server.ex @@ -49,7 +49,6 @@ defmodule ElixirLS.LanguageServer.Server do build_diagnostics: [], dialyzer_diagnostics: [], needs_build?: false, - load_all_mix_applications?: false, build_running?: false, analysis_ready?: false, received_shutdown?: false, @@ -923,19 +922,14 @@ defmodule ElixirLS.LanguageServer.Server do not state.build_running? -> fetch_deps? = Map.get(state.settings || %{}, "fetchDeps", false) - {_pid, build_ref} = - Build.build(self(), project_dir, - fetch_deps?: fetch_deps?, - load_all_mix_applications?: state.load_all_mix_applications? - ) + {_pid, build_ref} = Build.build(self(), project_dir, fetch_deps?: fetch_deps?) %__MODULE__{ state | build_ref: build_ref, needs_build?: false, build_running?: true, - analysis_ready?: false, - load_all_mix_applications?: false + analysis_ready?: false } true -> @@ -1217,7 +1211,7 @@ defmodule ElixirLS.LanguageServer.Server do is_nil(prev_project_dir) -> File.cd!(project_dir) - Map.merge(state, %{project_dir: File.cwd!(), load_all_mix_applications?: true}) + %{state | project_dir: File.cwd!()} prev_project_dir != project_dir -> JsonRpc.show_message( diff --git a/apps/language_server/test/providers/code_lens/test_test.exs b/apps/language_server/test/providers/code_lens/test_test.exs index c6e898fb1..ad7f56136 100644 --- a/apps/language_server/test/providers/code_lens/test_test.exs +++ b/apps/language_server/test/providers/code_lens/test_test.exs @@ -7,8 +7,6 @@ defmodule ElixirLS.LanguageServer.Providers.CodeLens.TestTest do @project_dir "/project" setup context do - ElixirLS.LanguageServer.Build.load_all_mix_applications() - unless context[:skip_server] do server = ElixirLS.LanguageServer.Test.ServerTestHelpers.start_server() diff --git a/apps/language_server/test/providers/completion_test.exs b/apps/language_server/test/providers/completion_test.exs index d4e1b01e2..b15069455 100644 --- a/apps/language_server/test/providers/completion_test.exs +++ b/apps/language_server/test/providers/completion_test.exs @@ -20,8 +20,6 @@ defmodule ElixirLS.LanguageServer.Providers.CompletionTest do } setup context do - ElixirLS.LanguageServer.Build.load_all_mix_applications() - unless context[:skip_server] do server = ElixirLS.LanguageServer.Test.ServerTestHelpers.start_server() From c3b73d82e91a591544f67a3dd6c880cc7aa40c12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Samson?= Date: Mon, 19 Sep 2022 08:06:59 +0200 Subject: [PATCH 055/125] Add mix clean custom command (#730) * rename compile * add clean * add mix clean as a custom command * add test * fix tests * fix tests --- .../lib/language_server/build.ex | 32 ++++++++++++- .../providers/execute_command.ex | 3 +- .../providers/execute_command/mix_clean.ex | 11 +++++ .../test/fixtures/clean/lib/a.ex | 5 ++ .../test/fixtures/clean/lib/b.ex | 5 ++ .../test/fixtures/clean/lib/c.ex | 5 ++ .../test/fixtures/clean/mix.exs | 15 ++++++ .../execute_command/mix_clean_test.exs | 47 +++++++++++++++++++ 8 files changed, 120 insertions(+), 3 deletions(-) create mode 100644 apps/language_server/lib/language_server/providers/execute_command/mix_clean.ex create mode 100644 apps/language_server/test/fixtures/clean/lib/a.ex create mode 100644 apps/language_server/test/fixtures/clean/lib/b.ex create mode 100644 apps/language_server/test/fixtures/clean/lib/c.ex create mode 100644 apps/language_server/test/fixtures/clean/mix.exs create mode 100644 apps/language_server/test/providers/execute_command/mix_clean_test.exs diff --git a/apps/language_server/lib/language_server/build.ex b/apps/language_server/lib/language_server/build.ex index 4dec17f05..b400019f2 100644 --- a/apps/language_server/lib/language_server/build.ex +++ b/apps/language_server/lib/language_server/build.ex @@ -27,7 +27,7 @@ defmodule ElixirLS.LanguageServer.Build do # if we won't do it elixir >= 1.11 warns that protocols have already been consolidated purge_consolidated_protocols() - {status, diagnostics} = compile() + {status, diagnostics} = run_mix_compile() diagnostics = Diagnostics.normalize(diagnostics, root_path) Server.build_finished(parent, {status, mixfile_diagnostics ++ diagnostics}) @@ -46,6 +46,13 @@ defmodule ElixirLS.LanguageServer.Build do end end + def clean(clean_deps? \\ false) do + with_build_lock(fn -> + Mix.Task.clear() + run_mix_clean(clean_deps?) + end) + end + def with_build_lock(func) do :global.trans({__MODULE__, self()}, func) end @@ -104,7 +111,8 @@ defmodule ElixirLS.LanguageServer.Build do end end - defp compile do + defp run_mix_compile do + # TODO consider adding --no-compile case Mix.Task.run("compile", ["--return-errors", "--ignore-module-conflict"]) do {status, diagnostics} when status in [:ok, :error, :noop] and is_list(diagnostics) -> {status, diagnostics} @@ -117,6 +125,26 @@ defmodule ElixirLS.LanguageServer.Build do end end + defp run_mix_clean(clean_deps?) do + opts = [] + + opts = + if clean_deps? do + opts ++ ["--deps"] + else + opts + end + + results = Mix.Task.run("clean", opts) |> List.wrap() + + if Enum.all?(results, &match?(:ok, &1)) do + :ok + else + JsonRpc.log_message(:error, "mix clean returned #{inspect(results)}") + {:error, :clean_failed} + end + end + defp purge_consolidated_protocols do config = Mix.Project.config() path = Mix.Project.consolidation_path(config) diff --git a/apps/language_server/lib/language_server/providers/execute_command.ex b/apps/language_server/lib/language_server/providers/execute_command.ex index 63b9fdc27..427521244 100644 --- a/apps/language_server/lib/language_server/providers/execute_command.ex +++ b/apps/language_server/lib/language_server/providers/execute_command.ex @@ -9,7 +9,8 @@ defmodule ElixirLS.LanguageServer.Providers.ExecuteCommand do "spec" => ExecuteCommand.ApplySpec, "expandMacro" => ExecuteCommand.ExpandMacro, "manipulatePipes" => ExecuteCommand.ManipulatePipes, - "restart" => ExecuteCommand.Restart + "restart" => ExecuteCommand.Restart, + "mixClean" => ExecuteCommand.MixClean } @callback execute([any], %ElixirLS.LanguageServer.Server{}) :: diff --git a/apps/language_server/lib/language_server/providers/execute_command/mix_clean.ex b/apps/language_server/lib/language_server/providers/execute_command/mix_clean.ex new file mode 100644 index 000000000..4edaa4022 --- /dev/null +++ b/apps/language_server/lib/language_server/providers/execute_command/mix_clean.ex @@ -0,0 +1,11 @@ +defmodule ElixirLS.LanguageServer.Providers.ExecuteCommand.MixClean do + @behaviour ElixirLS.LanguageServer.Providers.ExecuteCommand + + @impl ElixirLS.LanguageServer.Providers.ExecuteCommand + def execute([clean_deps?], _state) do + case ElixirLS.LanguageServer.Build.clean(clean_deps?) do + :ok -> {:ok, %{}} + {:error, reason} -> {:error, :server_error, inspect(reason)} + end + end +end diff --git a/apps/language_server/test/fixtures/clean/lib/a.ex b/apps/language_server/test/fixtures/clean/lib/a.ex new file mode 100644 index 000000000..ec7cb179f --- /dev/null +++ b/apps/language_server/test/fixtures/clean/lib/a.ex @@ -0,0 +1,5 @@ +defmodule A do + def fun do + :ok = B.fun() + end +end diff --git a/apps/language_server/test/fixtures/clean/lib/b.ex b/apps/language_server/test/fixtures/clean/lib/b.ex new file mode 100644 index 000000000..b7ce208dc --- /dev/null +++ b/apps/language_server/test/fixtures/clean/lib/b.ex @@ -0,0 +1,5 @@ +defmodule B do + def fun do + :error + end +end diff --git a/apps/language_server/test/fixtures/clean/lib/c.ex b/apps/language_server/test/fixtures/clean/lib/c.ex new file mode 100644 index 000000000..3cce833b8 --- /dev/null +++ b/apps/language_server/test/fixtures/clean/lib/c.ex @@ -0,0 +1,5 @@ +defmodule C do + def myfun do + 1 + end +end diff --git a/apps/language_server/test/fixtures/clean/mix.exs b/apps/language_server/test/fixtures/clean/mix.exs new file mode 100644 index 000000000..c8b5fc4dc --- /dev/null +++ b/apps/language_server/test/fixtures/clean/mix.exs @@ -0,0 +1,15 @@ +defmodule ElixirLS.LanguageServer.Fixtures.Clean.Mixfile do + use Mix.Project + + def project do + [app: :els_clean_test, version: "0.1.0"] + end + + # Configuration for the OTP application + # + # Type "mix help compile.app" for more information + def application do + # Specify extra applications you'll use from Erlang/Elixir + [] + end +end diff --git a/apps/language_server/test/providers/execute_command/mix_clean_test.exs b/apps/language_server/test/providers/execute_command/mix_clean_test.exs new file mode 100644 index 000000000..f9e55464d --- /dev/null +++ b/apps/language_server/test/providers/execute_command/mix_clean_test.exs @@ -0,0 +1,47 @@ +defmodule ElixirLS.LanguageServer.Providers.ExecuteCommand.MixCleanTest do + alias ElixirLS.LanguageServer.{Server, Protocol, SourceFile} + use ElixirLS.Utils.MixTest.Case, async: false + use Protocol + + setup do + server = ElixirLS.LanguageServer.Test.ServerTestHelpers.start_server() + + {:ok, %{server: server}} + end + + @tag fixture: true + test "mix clean", %{server: server} do + in_fixture(Path.join(__DIR__, "../.."), "clean", fn -> + root_uri = SourceFile.path_to_uri(File.cwd!()) + Server.receive_packet(server, initialize_req(1, root_uri, %{})) + + Server.receive_packet( + server, + did_change_configuration(%{ + "elixirLS" => %{"dialyzerEnabled" => false} + }) + ) + + assert_receive %{ + "method" => "window/logMessage", + "params" => %{"message" => "Compile took" <> _} + }, + 20000 + + path = ".elixir_ls/build/test/lib/els_clean_test/ebin/Elixir.A.beam" + assert File.exists?(path) + + server_instance_id = :sys.get_state(server).server_instance_id + + Server.receive_packet( + server, + execute_command_req(4, "mixClean:#{server_instance_id}", [false]) + ) + + res = assert_receive(%{"id" => 4}, 5000) + assert res["result"] == %{} + + refute File.exists?(path) + end) + end +end From e95dcc5df14c13a09560a31cb4a118f7b3a25b3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Samson?= Date: Wed, 21 Sep 2022 22:44:18 +0200 Subject: [PATCH 056/125] Introduce a compilation tracer (#724) * Initial implementation of tracer migrated to tracer for references added building of module and def database * server test fixed * fix dialyzer tests * fix references tests * add tracing of local calls * fix and extend definition tests * make suite synchronous * add test coverage to tracer * address todos * fix warnings * fix tests * make tests more predictable --- apps/language_server/lib/language_server.ex | 3 +- .../lib/language_server/build.ex | 21 +- .../lib/language_server/cli.ex | 3 + .../language_server/providers/references.ex | 4 +- .../lib/language_server/server.ex | 22 +- .../lib/language_server/tracer.ex | 335 ++++++++++++++++++ apps/language_server/test/dialyzer_test.exs | 10 +- .../test/providers/definition_test.exs | 180 +++++++++- .../execute_command/mix_clean_test.exs | 3 +- .../test/providers/references_test.exs | 133 +++++-- apps/language_server/test/server_test.exs | 16 +- .../test/support/fixtures/.elixir_ls/.gitkeep | 0 .../test/support/fixtures/references_a.ex | 5 - .../test/support/fixtures/references_b.ex | 8 - .../support/fixtures/references_imported.ex | 13 + .../support/fixtures/references_referenced.ex | 30 ++ .../support/fixtures/references_remote.ex | 13 + apps/language_server/test/tracer_test.exs | 209 +++++++++++ mix.lock | 2 +- 19 files changed, 951 insertions(+), 59 deletions(-) create mode 100644 apps/language_server/lib/language_server/tracer.ex create mode 100644 apps/language_server/test/support/fixtures/.elixir_ls/.gitkeep delete mode 100644 apps/language_server/test/support/fixtures/references_a.ex delete mode 100644 apps/language_server/test/support/fixtures/references_b.ex create mode 100644 apps/language_server/test/support/fixtures/references_imported.ex create mode 100644 apps/language_server/test/support/fixtures/references_referenced.ex create mode 100644 apps/language_server/test/support/fixtures/references_remote.ex create mode 100644 apps/language_server/test/tracer_test.exs diff --git a/apps/language_server/lib/language_server.ex b/apps/language_server/lib/language_server.ex index f913823ef..36dfd96c1 100644 --- a/apps/language_server/lib/language_server.ex +++ b/apps/language_server/lib/language_server.ex @@ -9,7 +9,8 @@ defmodule ElixirLS.LanguageServer do children = [ {ElixirLS.LanguageServer.Server, ElixirLS.LanguageServer.Server}, {ElixirLS.LanguageServer.JsonRpc, name: ElixirLS.LanguageServer.JsonRpc}, - {ElixirLS.LanguageServer.Providers.WorkspaceSymbols, []} + {ElixirLS.LanguageServer.Providers.WorkspaceSymbols, []}, + {ElixirLS.LanguageServer.Tracer, []} ] opts = [strategy: :one_for_one, name: ElixirLS.LanguageServer.Supervisor, max_restarts: 0] diff --git a/apps/language_server/lib/language_server/build.ex b/apps/language_server/lib/language_server/build.ex index b400019f2..622910dde 100644 --- a/apps/language_server/lib/language_server/build.ex +++ b/apps/language_server/lib/language_server/build.ex @@ -1,5 +1,5 @@ defmodule ElixirLS.LanguageServer.Build do - alias ElixirLS.LanguageServer.{Server, JsonRpc, Diagnostics} + alias ElixirLS.LanguageServer.{Server, JsonRpc, Diagnostics, Tracer} alias ElixirLS.Utils.MixfileHelpers def build(parent, root_path, opts) when is_binary(root_path) do @@ -40,6 +40,7 @@ defmodule ElixirLS.LanguageServer.Build do end end) + Tracer.save() JsonRpc.log_message(:info, "Compile took #{div(us, 1000)} milliseconds") end) end) @@ -213,4 +214,22 @@ defmodule ElixirLS.LanguageServer.Build do :ok end + + def set_compiler_options(options \\ [], parser_options \\ []) do + parser_options = + parser_options + |> Keyword.merge( + columns: true, + token_metadata: true + ) + + options = + options + |> Keyword.merge( + tracers: [Tracer], + parser_options: parser_options + ) + + Code.compiler_options(options) + end end diff --git a/apps/language_server/lib/language_server/cli.ex b/apps/language_server/lib/language_server/cli.ex index d66815a33..f747e5841 100644 --- a/apps/language_server/lib/language_server/cli.ex +++ b/apps/language_server/lib/language_server/cli.ex @@ -1,11 +1,14 @@ defmodule ElixirLS.LanguageServer.CLI do alias ElixirLS.Utils.{WireProtocol, Launch} alias ElixirLS.LanguageServer.JsonRpc + alias ElixirLS.LanguageServer.Build def main do WireProtocol.intercept_output(&JsonRpc.print/1, &JsonRpc.print_err/1) Launch.start_mix() + Build.set_compiler_options() + start_language_server() IO.puts("Started ElixirLS v#{Launch.language_server_version()}") diff --git a/apps/language_server/lib/language_server/providers/references.ex b/apps/language_server/lib/language_server/providers/references.ex index 7188ac608..2ca5ae513 100644 --- a/apps/language_server/lib/language_server/providers/references.ex +++ b/apps/language_server/lib/language_server/providers/references.ex @@ -17,7 +17,9 @@ defmodule ElixirLS.LanguageServer.Providers.References do {line, character} = SourceFile.lsp_position_to_elixir(text, {line, character}) Build.with_build_lock(fn -> - ElixirSense.references(text, line, character) + trace = ElixirLS.LanguageServer.Tracer.get_trace() + + ElixirSense.references(text, line, character, trace) |> Enum.map(fn elixir_sense_reference -> elixir_sense_reference |> build_reference(uri, text) diff --git a/apps/language_server/lib/language_server/server.ex b/apps/language_server/lib/language_server/server.ex index 37abaddac..d5fef5c7d 100644 --- a/apps/language_server/lib/language_server/server.ex +++ b/apps/language_server/lib/language_server/server.ex @@ -35,6 +35,7 @@ defmodule ElixirLS.LanguageServer.Server do } alias ElixirLS.Utils.Launch + alias ElixirLS.LanguageServer.Tracer use Protocol @@ -57,6 +58,7 @@ defmodule ElixirLS.LanguageServer.Server do source_files: %{}, awaiting_contracts: [], supports_dynamic: false, + mix_project?: false, no_mixfile_warned?: false ] @@ -415,6 +417,17 @@ defmodule ElixirLS.LanguageServer.Server do state.source_files[uri].dirty?) end) + # TODO remove uniq when duplicated subscriptions from vscode plugin are fixed + deleted_paths = + for change <- changes, + change["type"] == 3, + uniq: true, + do: SourceFile.path_from_uri(change["uri"]) + + for path <- deleted_paths do + Tracer.notify_file_deleted(path) + end + source_files = changes |> Enum.reduce(state.source_files, fn @@ -447,6 +460,7 @@ defmodule ElixirLS.LanguageServer.Server do state = %{state | source_files: source_files} + # TODO remove uniq when duplicated subscriptions from vscode plugin are fixed changes |> Enum.map(& &1["uri"]) |> Enum.uniq() @@ -1043,7 +1057,10 @@ defmodule ElixirLS.LanguageServer.Server do text {:error, reason} -> - IO.warn("Couldn't read file #{file}: #{inspect(reason)}") + if reason != :enoent do + IO.warn("Couldn't read file #{file}: #{inspect(reason)}") + end + nil end end @@ -1099,6 +1116,7 @@ defmodule ElixirLS.LanguageServer.Server do |> add_watched_extensions(additional_watched_extensions) state = create_gitignore(state) + Tracer.set_project_dir(state.project_dir) trigger_build(%{state | settings: settings}) end @@ -1211,7 +1229,7 @@ defmodule ElixirLS.LanguageServer.Server do is_nil(prev_project_dir) -> File.cd!(project_dir) - %{state | project_dir: File.cwd!()} + %{state | project_dir: File.cwd!(), mix_project?: File.exists?("mix.exs")} prev_project_dir != project_dir -> JsonRpc.show_message( diff --git a/apps/language_server/lib/language_server/tracer.ex b/apps/language_server/lib/language_server/tracer.ex new file mode 100644 index 000000000..dc2d1d618 --- /dev/null +++ b/apps/language_server/lib/language_server/tracer.ex @@ -0,0 +1,335 @@ +defmodule ElixirLS.LanguageServer.Tracer do + @moduledoc """ + """ + use GenServer + require Logger + + @tables ~w(modules calls)a + + for table <- @tables do + defp table_name(unquote(table)) do + :"#{__MODULE__}:#{unquote(table)}" + end + end + + def start_link(args) do + GenServer.start_link(__MODULE__, args, name: __MODULE__) + end + + def set_project_dir(project_dir) do + GenServer.call(__MODULE__, {:set_project_dir, project_dir}) + end + + defp get_project_dir() do + case Process.get(:elixir_ls_project_dir) do + nil -> + project_dir = GenServer.call(__MODULE__, :get_project_dir) + Process.put(:elixir_ls_project_dir, project_dir) + project_dir + + project_dir -> + project_dir + end + end + + def notify_file_deleted(file) do + delete_modules_by_file(file) + delete_calls_by_file(file) + end + + @impl true + def init(_args) do + for table <- @tables do + table_name = table_name(table) + + :ets.new(table_name, [ + :named_table, + :public, + read_concurrency: true, + write_concurrency: true + ]) + end + + {:ok, %{project_dir: nil}} + end + + @impl true + def handle_call({:set_project_dir, project_dir}, _from, state) do + maybe_close_tables(state) + + for table <- @tables do + table_name = table_name(table) + :ets.delete_all_objects(table_name) + end + + if project_dir != nil do + {us, _} = + :timer.tc(fn -> + for table <- @tables do + init_table(table, project_dir) + end + end) + + Logger.info("Loaded DETS databases in #{div(us, 1000)}ms") + end + + {:reply, :ok, %{state | project_dir: project_dir}} + end + + def handle_call(:get_project_dir, _from, %{project_dir: project_dir} = state) do + {:reply, project_dir, state} + end + + @impl true + def terminate(_reason, state) do + maybe_close_tables(state) + end + + defp maybe_close_tables(%{project_dir: nil}), do: :ok + + defp maybe_close_tables(%{project_dir: project_dir}) do + for table <- @tables do + close_table(table, project_dir) + end + + :ok + end + + defp dets_path(project_dir, table) do + Path.join([project_dir, ".elixir_ls", "#{table}.dets"]) + end + + def init_table(table, project_dir) do + table_name = table_name(table) + path = dets_path(project_dir, table) + + {:ok, _} = + :dets.open_file(table_name, + file: path |> String.to_charlist(), + auto_save: 60_000 + ) + + case :dets.to_ets(table_name, table_name) do + ^table_name -> + :ok + + {:error, reason} -> + Logger.error("Unable to load DETS #{path}, #{inspect(reason)}") + end + end + + def close_table(table, project_dir) do + path = dets_path(project_dir, table) + table_name = table_name(table) + sync(table_name) + + case :dets.close(table_name) do + :ok -> + :ok + + {:error, reason} -> + Logger.error("Unable to close DETS #{path}, #{inspect(reason)}") + end + end + + defp modules_by_file_matchspec(file, return) do + [ + {{:"$1", :"$2"}, + [ + { + :andalso, + {:andalso, {:==, {:map_get, :file, :"$2"}, file}} + } + ], [return]} + ] + end + + def get_modules_by_file(file) do + ms = modules_by_file_matchspec(file, :"$_") + # ms = :ets.fun2ms(fn {_, map} when :erlang.map_get(:file, map) == file -> map end) + + :ets.select(table_name(:modules), ms) + end + + def delete_modules_by_file(file) do + ms = modules_by_file_matchspec(file, true) + # ms = :ets.fun2ms(fn {_, map} when :erlang.map_get(:file, map) == file -> true end) + + :ets.select_delete(table_name(:modules), ms) + end + + def trace(:start, %Macro.Env{} = env) do + delete_modules_by_file(env.file) + delete_calls_by_file(env.file) + :ok + end + + def trace({:on_module, _, _}, %Macro.Env{} = env) do + info = build_module_info(env.module, env.file, env.line) + :ets.insert(table_name(:modules), {env.module, info}) + :ok + end + + def trace({kind, meta, module, name, arity}, %Macro.Env{} = env) + when kind in [:imported_function, :imported_macro, :remote_function, :remote_macro] do + register_call(meta, module, name, arity, env) + end + + def trace({kind, meta, name, arity}, %Macro.Env{} = env) + when kind in [:local_function, :local_macro] do + register_call(meta, env.module, name, arity, env) + end + + def trace(_trace, _env) do + # IO.inspect(trace, label: "skipped") + :ok + end + + defp build_module_info(module, file, line) do + defs = + for {name, arity} <- Module.definitions_in(module) do + def_info = Module.get_definition(module, {name, arity}) + {{name, arity}, build_def_info(def_info)} + end + + attributes = + for name <- Module.attributes_in(module) do + {name, Module.get_attribute(module, name)} + end + + %{ + defs: defs, + attributes: attributes, + file: file, + line: line + } + end + + defp build_def_info({:v1, def_kind, meta_1, clauses}) do + clauses = + for {meta_2, arguments, guards, _body} <- clauses do + %{ + arguments: arguments, + guards: guards, + meta: meta_2 + } + end + + %{ + kind: def_kind, + clauses: clauses, + meta: meta_1 + } + end + + defp register_call(meta, module, name, arity, env) do + if in_project_sources?(env.file) do + do_register_call(meta, module, name, arity, env) + end + + :ok + end + + defp do_register_call(meta, module, name, arity, env) do + callee = {module, name, arity} + + call = %{ + callee: callee, + file: env.file, + line: meta[:line], + column: meta[:column] + } + + updated_calls = + case :ets.lookup(table_name(:calls), callee) do + [{_callee, callee_calls}] when is_map(callee_calls) -> + file_calle_calls = + case callee_calls[env.file] do + nil -> [call] + file_calle_calls -> [call | file_calle_calls] + end + + Map.put(callee_calls, env.file, file_calle_calls) + + [] -> + %{env.file => [call]} + end + + :ets.insert(table_name(:calls), {callee, updated_calls}) + end + + def get_trace do + :ets.tab2list(table_name(:calls)) + |> Map.new(fn {callee, calls_by_file} -> + calls = calls_by_file |> Map.values() |> List.flatten() + {callee, calls} + end) + end + + def save do + for table <- @tables do + table_name = table_name(table) + + sync(table_name) + end + end + + defp sync(table_name) do + with :ok <- :dets.from_ets(table_name, table_name), + :ok <- :dets.sync(table_name) do + :ok + else + {:error, reason} -> + Logger.error("Unable to sync DETS #{table_name}, #{inspect(reason)}") + end + end + + defp in_project_sources?(path) do + project_dir = get_project_dir() + + if project_dir != nil do + topmost_path_segment = + path + |> Path.relative_to(project_dir) + |> Path.split() + |> hd + + topmost_path_segment != "deps" + else + false + end + end + + defp calls_by_file_matchspec(file, return) do + [ + {{:"$1", :"$2"}, + [ + { + :andalso, + {:andalso, {:is_list, {:map_get, file, :"$2"}}} + } + ], [return]} + ] + end + + def get_calls_by_file(file) do + ms = calls_by_file_matchspec(file, :"$_") + + :ets.select(table_name(:calls), ms) + end + + def delete_calls_by_file(file) do + ms = calls_by_file_matchspec(file, :"$_") + table_name = table_name(:calls) + + for {callee, calls_by_file} <- :ets.select(table_name(:calls), ms) do + calls_by_file = calls_by_file |> Map.delete(file) + + if calls_by_file == %{} do + :ets.delete(table_name, callee) + else + :ets.insert(table_name, {callee, calls_by_file}) + end + end + end +end diff --git a/apps/language_server/test/dialyzer_test.exs b/apps/language_server/test/dialyzer_test.exs index fa4d516af..2dfc944dd 100644 --- a/apps/language_server/test/dialyzer_test.exs +++ b/apps/language_server/test/dialyzer_test.exs @@ -1,7 +1,7 @@ defmodule ElixirLS.LanguageServer.DialyzerTest do # TODO: Test loading and saving manifest - alias ElixirLS.LanguageServer.{Dialyzer, Server, Protocol, SourceFile, JsonRpc} + alias ElixirLS.LanguageServer.{Dialyzer, Server, Protocol, SourceFile, JsonRpc, Tracer, Build} import ExUnit.CaptureLog use ElixirLS.Utils.MixTest.Case, async: false use Protocol @@ -10,10 +10,18 @@ defmodule ElixirLS.LanguageServer.DialyzerTest do # This will generate a large PLT file and will take a long time, so we need to make sure that # Mix.Utils.home() is in the saved build artifacts for any automated testing Dialyzer.Manifest.load_elixir_plt() + compiler_options = Code.compiler_options() + Build.set_compiler_options() + + on_exit(fn -> + Code.compiler_options(compiler_options) + end) + {:ok, %{}} end setup do + {:ok, _} = Tracer.start_link([]) server = ElixirLS.LanguageServer.Test.ServerTestHelpers.start_server() {:ok, %{server: server}} diff --git a/apps/language_server/test/providers/definition_test.exs b/apps/language_server/test/providers/definition_test.exs index f5933fe26..16ef598f8 100644 --- a/apps/language_server/test/providers/definition_test.exs +++ b/apps/language_server/test/providers/definition_test.exs @@ -7,19 +7,19 @@ defmodule ElixirLS.LanguageServer.Providers.DefinitionTest do alias ElixirLS.LanguageServer.Test.FixtureHelpers require ElixirLS.Test.TextLoc - test "find definition" do - file_path = FixtureHelpers.get_path("references_a.ex") + test "find definition remote function call" do + file_path = FixtureHelpers.get_path("references_remote.ex") text = File.read!(file_path) uri = SourceFile.path_to_uri(file_path) - b_file_path = FixtureHelpers.get_path("references_b.ex") + b_file_path = FixtureHelpers.get_path("references_referenced.ex") b_uri = SourceFile.path_to_uri(b_file_path) - {line, char} = {2, 30} + {line, char} = {4, 28} ElixirLS.Test.TextLoc.annotate_assert(file_path, line, char, """ - ElixirLS.Test.ReferencesB.b_fun() - ^ + ReferencesReferenced.referenced_fun() + ^ """) assert {:ok, %Location{uri: ^b_uri, range: range}} = @@ -30,4 +30,172 @@ defmodule ElixirLS.LanguageServer.Providers.DefinitionTest do "end" => %{"line" => 1, "character" => 6} } end + + test "find definition remote macro call" do + file_path = FixtureHelpers.get_path("references_remote.ex") + text = File.read!(file_path) + uri = SourceFile.path_to_uri(file_path) + + b_file_path = FixtureHelpers.get_path("references_referenced.ex") + b_uri = SourceFile.path_to_uri(b_file_path) + + {line, char} = {8, 28} + + ElixirLS.Test.TextLoc.annotate_assert(file_path, line, char, """ + ReferencesReferenced.referenced_macro a do + ^ + """) + + assert {:ok, %Location{uri: ^b_uri, range: range}} = + Definition.definition(uri, text, line, char) + + assert range == %{ + "start" => %{"line" => 8, "character" => 11}, + "end" => %{"line" => 8, "character" => 11} + } + end + + test "find definition imported function call" do + file_path = FixtureHelpers.get_path("references_imported.ex") + text = File.read!(file_path) + uri = SourceFile.path_to_uri(file_path) + + b_file_path = FixtureHelpers.get_path("references_referenced.ex") + b_uri = SourceFile.path_to_uri(b_file_path) + + {line, char} = {4, 5} + + ElixirLS.Test.TextLoc.annotate_assert(file_path, line, char, """ + referenced_fun() + ^ + """) + + assert {:ok, %Location{uri: ^b_uri, range: range}} = + Definition.definition(uri, text, line, char) + + assert range == %{ + "start" => %{"line" => 1, "character" => 6}, + "end" => %{"line" => 1, "character" => 6} + } + end + + test "find definition imported macro call" do + file_path = FixtureHelpers.get_path("references_imported.ex") + text = File.read!(file_path) + uri = SourceFile.path_to_uri(file_path) + + b_file_path = FixtureHelpers.get_path("references_referenced.ex") + b_uri = SourceFile.path_to_uri(b_file_path) + + {line, char} = {8, 5} + + ElixirLS.Test.TextLoc.annotate_assert(file_path, line, char, """ + referenced_macro a do + ^ + """) + + assert {:ok, %Location{uri: ^b_uri, range: range}} = + Definition.definition(uri, text, line, char) + + assert range == %{ + "start" => %{"line" => 8, "character" => 11}, + "end" => %{"line" => 8, "character" => 11} + } + end + + test "find definition local function call" do + file_path = FixtureHelpers.get_path("references_referenced.ex") + text = File.read!(file_path) + uri = SourceFile.path_to_uri(file_path) + + b_file_path = FixtureHelpers.get_path("references_referenced.ex") + b_uri = SourceFile.path_to_uri(b_file_path) + + {line, char} = {15, 5} + + ElixirLS.Test.TextLoc.annotate_assert(file_path, line, char, """ + referenced_fun() + ^ + """) + + assert {:ok, %Location{uri: ^b_uri, range: range}} = + Definition.definition(uri, text, line, char) + + assert range == %{ + "start" => %{"line" => 1, "character" => 6}, + "end" => %{"line" => 1, "character" => 6} + } + end + + test "find definition local macro call" do + file_path = FixtureHelpers.get_path("references_referenced.ex") + text = File.read!(file_path) + uri = SourceFile.path_to_uri(file_path) + + b_file_path = FixtureHelpers.get_path("references_referenced.ex") + b_uri = SourceFile.path_to_uri(b_file_path) + + {line, char} = {19, 5} + + ElixirLS.Test.TextLoc.annotate_assert(file_path, line, char, """ + referenced_macro a do + ^ + """) + + assert {:ok, %Location{uri: ^b_uri, range: range}} = + Definition.definition(uri, text, line, char) + + assert range == %{ + "start" => %{"line" => 8, "character" => 11}, + "end" => %{"line" => 8, "character" => 11} + } + end + + test "find definition variable" do + file_path = FixtureHelpers.get_path("references_referenced.ex") + text = File.read!(file_path) + uri = SourceFile.path_to_uri(file_path) + + b_file_path = FixtureHelpers.get_path("references_referenced.ex") + b_uri = SourceFile.path_to_uri(b_file_path) + + {line, char} = {4, 13} + + ElixirLS.Test.TextLoc.annotate_assert(file_path, line, char, """ + IO.puts(referenced_variable + 1) + ^ + """) + + assert {:ok, %Location{uri: ^b_uri, range: range}} = + Definition.definition(uri, text, line, char) + + assert range == %{ + "start" => %{"line" => 2, "character" => 4}, + "end" => %{"line" => 2, "character" => 4} + } + end + + test "find definition attribute" do + file_path = FixtureHelpers.get_path("references_referenced.ex") + text = File.read!(file_path) + uri = SourceFile.path_to_uri(file_path) + + b_file_path = FixtureHelpers.get_path("references_referenced.ex") + b_uri = SourceFile.path_to_uri(b_file_path) + + {line, char} = {27, 5} + + ElixirLS.Test.TextLoc.annotate_assert(file_path, line, char, """ + @referenced_attribute + ^ + """) + + assert {:ok, %Location{uri: ^b_uri, range: range}} = + Definition.definition(uri, text, line, char) + + assert range == %{ + "start" => %{"line" => 24, "character" => 2}, + "end" => %{"line" => 24, "character" => 2} + } + end end diff --git a/apps/language_server/test/providers/execute_command/mix_clean_test.exs b/apps/language_server/test/providers/execute_command/mix_clean_test.exs index f9e55464d..a82cfe529 100644 --- a/apps/language_server/test/providers/execute_command/mix_clean_test.exs +++ b/apps/language_server/test/providers/execute_command/mix_clean_test.exs @@ -1,9 +1,10 @@ defmodule ElixirLS.LanguageServer.Providers.ExecuteCommand.MixCleanTest do - alias ElixirLS.LanguageServer.{Server, Protocol, SourceFile} + alias ElixirLS.LanguageServer.{Server, Protocol, SourceFile, Tracer} use ElixirLS.Utils.MixTest.Case, async: false use Protocol setup do + {:ok, _} = Tracer.start_link([]) server = ElixirLS.LanguageServer.Test.ServerTestHelpers.start_server() {:ok, %{server: server}} diff --git a/apps/language_server/test/providers/references_test.exs b/apps/language_server/test/providers/references_test.exs index 334a9e55b..3882f4bc9 100644 --- a/apps/language_server/test/providers/references_test.exs +++ b/apps/language_server/test/providers/references_test.exs @@ -1,45 +1,81 @@ defmodule ElixirLS.LanguageServer.Providers.ReferencesTest do - use ExUnit.Case, async: true + use ExUnit.Case, async: false alias ElixirLS.LanguageServer.Providers.References alias ElixirLS.LanguageServer.SourceFile alias ElixirLS.LanguageServer.Test.FixtureHelpers + alias ElixirLS.LanguageServer.Tracer + alias ElixirLS.LanguageServer.Build require ElixirLS.Test.TextLoc - test "finds references to a function" do - file_path = FixtureHelpers.get_path("references_b.ex") + setup_all context do + File.rm_rf!(FixtureHelpers.get_path(".elixir_ls/calls.dets")) + {:ok, pid} = Tracer.start_link([]) + Tracer.set_project_dir(FixtureHelpers.get_path("")) + + compiler_options = Code.compiler_options() + Build.set_compiler_options(ignore_module_conflict: true) + + on_exit(fn -> + Code.compiler_options(compiler_options) + Process.monitor(pid) + Process.unlink(pid) + GenServer.stop(pid) + + receive do + {:DOWN, _, _, _, _} -> :ok + end + end) + + Code.compile_file(FixtureHelpers.get_path("references_referenced.ex")) + Code.compile_file(FixtureHelpers.get_path("references_imported.ex")) + Code.compile_file(FixtureHelpers.get_path("references_remote.ex")) + Code.compile_file(FixtureHelpers.get_path("uses_macro_a.ex")) + Code.compile_file(FixtureHelpers.get_path("macro_a.ex")) + {:ok, context} + end + + test "finds local, remote and imported references to a function" do + file_path = FixtureHelpers.get_path("references_referenced.ex") text = File.read!(file_path) uri = SourceFile.path_to_uri(file_path) - {line, char} = {2, 8} + {line, char} = {1, 8} ElixirLS.Test.TextLoc.annotate_assert(file_path, line, char, """ - some_var = 42 + def referenced_fun do ^ """) - ElixirLS.Utils.TestUtils.assert_match_list( - References.references(text, uri, line, char, true), - [ - %{ - "range" => %{ - "start" => %{"line" => 2, "character" => 4}, - "end" => %{"line" => 2, "character" => 12} - }, - "uri" => uri - }, - %{ - "range" => %{ - "start" => %{"line" => 4, "character" => 12}, - "end" => %{"line" => 4, "character" => 20} - }, - "uri" => uri - } - ] - ) + list = References.references(text, uri, line, char, true) + + assert length(list) == 3 + assert Enum.any?(list, &(&1["uri"] |> String.ends_with?("references_remote.ex"))) + assert Enum.any?(list, &(&1["uri"] |> String.ends_with?("references_imported.ex"))) + assert Enum.any?(list, &(&1["uri"] |> String.ends_with?("references_referenced.ex"))) + end + + test "finds local, remote and imported references to a macro" do + file_path = FixtureHelpers.get_path("references_referenced.ex") + text = File.read!(file_path) + uri = SourceFile.path_to_uri(file_path) + + {line, char} = {8, 12} + + ElixirLS.Test.TextLoc.annotate_assert(file_path, line, char, """ + defmacro referenced_macro(clause, do: expression) do + ^ + """) + + list = References.references(text, uri, line, char, true) + + assert length(list) == 3 + assert Enum.any?(list, &(&1["uri"] |> String.ends_with?("references_remote.ex"))) + assert Enum.any?(list, &(&1["uri"] |> String.ends_with?("references_imported.ex"))) + assert Enum.any?(list, &(&1["uri"] |> String.ends_with?("references_referenced.ex"))) end - test "cannot find a references to a macro generated function call" do + test "find a references to a macro generated function call" do file_path = FixtureHelpers.get_path("uses_macro_a.ex") text = File.read!(file_path) uri = SourceFile.path_to_uri(file_path) @@ -50,7 +86,15 @@ defmodule ElixirLS.LanguageServer.Providers.ReferencesTest do ^ """) - assert References.references(text, uri, line, char, true) == [] + assert References.references(text, uri, line, char, true) == [ + %{ + "range" => %{ + "end" => %{"character" => 16, "line" => 6}, + "start" => %{"character" => 4, "line" => 6} + }, + "uri" => uri + } + ] end test "finds a references to a macro imported function call" do @@ -76,31 +120,60 @@ defmodule ElixirLS.LanguageServer.Providers.ReferencesTest do end test "finds references to a variable" do - file_path = FixtureHelpers.get_path("references_b.ex") + file_path = FixtureHelpers.get_path("references_referenced.ex") text = File.read!(file_path) uri = SourceFile.path_to_uri(file_path) {line, char} = {4, 14} ElixirLS.Test.TextLoc.annotate_assert(file_path, line, char, """ - IO.puts(some_var + 1) + IO.puts(referenced_variable + 1) ^ """) assert References.references(text, uri, line, char, true) == [ %{ "range" => %{ - "end" => %{"character" => 12, "line" => 2}, + "end" => %{"character" => 23, "line" => 2}, "start" => %{"character" => 4, "line" => 2} }, "uri" => uri }, %{ "range" => %{ - "end" => %{"character" => 20, "line" => 4}, + "end" => %{"character" => 31, "line" => 4}, "start" => %{"character" => 12, "line" => 4} }, "uri" => uri } ] end + + test "finds references to an attribute" do + file_path = FixtureHelpers.get_path("references_referenced.ex") + text = File.read!(file_path) + uri = SourceFile.path_to_uri(file_path) + {line, char} = {24, 5} + + ElixirLS.Test.TextLoc.annotate_assert(file_path, line, char, """ + @referenced_attribute \"123\" + ^ + """) + + assert References.references(text, uri, line, char, true) == [ + %{ + "range" => %{ + "end" => %{"character" => 23, "line" => 24}, + "start" => %{"character" => 2, "line" => 24} + }, + "uri" => uri + }, + %{ + "range" => %{ + "end" => %{"character" => 25, "line" => 27}, + "start" => %{"character" => 4, "line" => 27} + }, + "uri" => uri + } + ] + end end diff --git a/apps/language_server/test/server_test.exs b/apps/language_server/test/server_test.exs index c9faf3b0e..e3d5284c4 100644 --- a/apps/language_server/test/server_test.exs +++ b/apps/language_server/test/server_test.exs @@ -1,5 +1,5 @@ defmodule ElixirLS.LanguageServer.ServerTest do - alias ElixirLS.LanguageServer.{Server, Protocol, SourceFile} + alias ElixirLS.LanguageServer.{Server, Protocol, SourceFile, Tracer, Build} alias ElixirLS.Utils.PacketCapture alias ElixirLS.LanguageServer.Test.FixtureHelpers use ElixirLS.Utils.MixTest.Case, async: false @@ -231,8 +231,9 @@ defmodule ElixirLS.LanguageServer.ServerTest do setup context do unless context[:skip_server] do server = ElixirLS.LanguageServer.Test.ServerTestHelpers.start_server() + {:ok, tracer} = Tracer.start_link([]) - {:ok, %{server: server}} + {:ok, %{server: server, tracer: tracer}} else :ok end @@ -1100,7 +1101,10 @@ defmodule ElixirLS.LanguageServer.ServerTest do text = File.read!(file_path) reference_uri = SourceFile.path_to_uri("lib/a.ex") + Build.set_compiler_options() + initialize(server) + wait_until_compiled(server) Server.receive_packet(server, did_open(file_uri, "elixir", 1, text)) Server.receive_packet( @@ -1119,6 +1123,8 @@ defmodule ElixirLS.LanguageServer.ServerTest do wait_until_compiled(server) end) + after + Code.put_compiler_option(:tracers, []) end @tag :fixture @@ -1129,7 +1135,10 @@ defmodule ElixirLS.LanguageServer.ServerTest do text = File.read!(file_path) reference_uri = SourceFile.path_to_uri("apps/app1/lib/app1.ex") + Build.set_compiler_options() + initialize(server) + wait_until_compiled(server) Server.receive_packet(server, did_open(file_uri, "elixir", 1, text)) Server.receive_packet( @@ -1148,6 +1157,8 @@ defmodule ElixirLS.LanguageServer.ServerTest do wait_until_compiled(server) end) + after + Code.put_compiler_option(:tracers, []) end @tag fixture: true, skip_server: true @@ -1157,6 +1168,7 @@ defmodule ElixirLS.LanguageServer.ServerTest do # First to compile the applications and build the cache. # Second time to see if loads modules with_new_server(fn server -> + {:ok, _pid} = Tracer.start_link([]) initialize(server) wait_until_compiled(server) end) diff --git a/apps/language_server/test/support/fixtures/.elixir_ls/.gitkeep b/apps/language_server/test/support/fixtures/.elixir_ls/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/apps/language_server/test/support/fixtures/references_a.ex b/apps/language_server/test/support/fixtures/references_a.ex deleted file mode 100644 index c9087d4d6..000000000 --- a/apps/language_server/test/support/fixtures/references_a.ex +++ /dev/null @@ -1,5 +0,0 @@ -defmodule ElixirLS.Test.ReferencesA do - def a_fun do - ElixirLS.Test.ReferencesB.b_fun() - end -end diff --git a/apps/language_server/test/support/fixtures/references_b.ex b/apps/language_server/test/support/fixtures/references_b.ex deleted file mode 100644 index 0bbb1f3c6..000000000 --- a/apps/language_server/test/support/fixtures/references_b.ex +++ /dev/null @@ -1,8 +0,0 @@ -defmodule ElixirLS.Test.ReferencesB do - def b_fun do - some_var = 42 - - IO.puts(some_var + 1) - :ok - end -end diff --git a/apps/language_server/test/support/fixtures/references_imported.ex b/apps/language_server/test/support/fixtures/references_imported.ex new file mode 100644 index 000000000..ae20fd4c8 --- /dev/null +++ b/apps/language_server/test/support/fixtures/references_imported.ex @@ -0,0 +1,13 @@ +defmodule ElixirLS.Test.ReferencesImported do + import ElixirLS.Test.ReferencesReferenced + + def uses_fun do + referenced_fun() + end + + def uses_macro(a) do + referenced_macro a do + :ok + end + end +end diff --git a/apps/language_server/test/support/fixtures/references_referenced.ex b/apps/language_server/test/support/fixtures/references_referenced.ex new file mode 100644 index 000000000..7f422cdc0 --- /dev/null +++ b/apps/language_server/test/support/fixtures/references_referenced.ex @@ -0,0 +1,30 @@ +defmodule ElixirLS.Test.ReferencesReferenced do + def referenced_fun do + referenced_variable = 42 + + IO.puts(referenced_variable + 1) + :ok + end + + defmacro referenced_macro(clause, do: expression) do + quote do + if(!unquote(clause), do: unquote(expression)) + end + end + + def uses_fun(_a) do + referenced_fun() + end + + def uses_macro(a) do + referenced_macro a do + :ok + end + end + + @referenced_attribute "123" + + def uses_attribute do + @referenced_attribute + end +end diff --git a/apps/language_server/test/support/fixtures/references_remote.ex b/apps/language_server/test/support/fixtures/references_remote.ex new file mode 100644 index 000000000..409d21201 --- /dev/null +++ b/apps/language_server/test/support/fixtures/references_remote.ex @@ -0,0 +1,13 @@ +defmodule ElixirLS.Test.ReferencesRemote do + require ElixirLS.Test.ReferencesReferenced, as: ReferencesReferenced + + def uses_fun do + ReferencesReferenced.referenced_fun() + end + + def uses_macro(a) do + ReferencesReferenced.referenced_macro a do + :ok + end + end +end diff --git a/apps/language_server/test/tracer_test.exs b/apps/language_server/test/tracer_test.exs new file mode 100644 index 000000000..4840cfee9 --- /dev/null +++ b/apps/language_server/test/tracer_test.exs @@ -0,0 +1,209 @@ +defmodule ElixirLS.LanguageServer.TracerTest do + use ExUnit.Case, async: false + alias ElixirLS.LanguageServer.Tracer + alias ElixirLS.LanguageServer.Test.FixtureHelpers + + setup context do + File.rm_rf!(FixtureHelpers.get_path(".elixir_ls/calls.dets")) + File.rm_rf!(FixtureHelpers.get_path(".elixir_ls/modules.dets")) + {:ok, _pid} = Tracer.start_link([]) + + {:ok, context} + end + + test "project dir is nil" do + assert GenServer.call(Tracer, :get_project_dir) == nil + end + + test "set project dir" do + project_path = FixtureHelpers.get_path("") + + Tracer.set_project_dir(project_path) + + assert GenServer.call(Tracer, :get_project_dir) == project_path + end + + test "saves DETS" do + Tracer.set_project_dir(FixtureHelpers.get_path("")) + + Tracer.save() + + assert File.exists?(FixtureHelpers.get_path(".elixir_ls/calls.dets")) + assert File.exists?(FixtureHelpers.get_path(".elixir_ls/modules.dets")) + end + + describe "call trace" do + setup context do + Tracer.set_project_dir(FixtureHelpers.get_path("")) + + {:ok, context} + end + + defp sorted_calls() do + :ets.tab2list(:"#{Tracer}:calls") |> Enum.sort() + end + + test "trace is empty" do + assert sorted_calls() == [] + end + + test "registers calls same function different files" do + Tracer.trace( + {:remote_function, [line: 12, column: 2], CalledModule, :called, 1}, + %Macro.Env{ + module: CallingModule, + file: "calling_module.ex" + } + ) + + Tracer.trace( + {:remote_function, [line: 13, column: 3], CalledModule, :called, 1}, + %Macro.Env{ + module: OtherCallingModule, + file: "other_calling_module.ex" + } + ) + + assert [ + {{CalledModule, :called, 1}, + %{ + "calling_module.ex" => [ + %{ + callee: {CalledModule, :called, 1}, + column: 2, + file: "calling_module.ex", + line: 12 + } + ], + "other_calling_module.ex" => [ + %{ + callee: {CalledModule, :called, 1}, + column: 3, + file: "other_calling_module.ex", + line: 13 + } + ] + }} + ] == sorted_calls() + end + + test "registers calls same function in one file" do + Tracer.trace( + {:remote_function, [line: 12, column: 2], CalledModule, :called, 1}, + %Macro.Env{ + module: CallingModule, + file: "calling_module.ex" + } + ) + + Tracer.trace( + {:remote_function, [line: 13, column: 3], CalledModule, :called, 1}, + %Macro.Env{ + module: CallingModule, + file: "calling_module.ex" + } + ) + + assert [ + {{CalledModule, :called, 1}, + %{ + "calling_module.ex" => [ + %{ + callee: {CalledModule, :called, 1}, + column: 3, + file: "calling_module.ex", + line: 13 + }, + %{ + callee: {CalledModule, :called, 1}, + column: 2, + file: "calling_module.ex", + line: 12 + } + ] + }} + ] == sorted_calls() + end + + test "registers calls different functions" do + Tracer.trace( + {:remote_function, [line: 12, column: 2], CalledModule, :called, 1}, + %Macro.Env{ + module: CallingModule, + file: "calling_module.ex" + } + ) + + Tracer.trace( + {:remote_function, [line: 13, column: 3], CalledModule, :other_called, 1}, + %Macro.Env{ + module: OtherCallingModule, + file: "other_calling_module.ex" + } + ) + + assert [ + {{CalledModule, :called, 1}, + %{ + "calling_module.ex" => [ + %{ + callee: {CalledModule, :called, 1}, + column: 2, + file: "calling_module.ex", + line: 12 + } + ] + }}, + {{CalledModule, :other_called, 1}, + %{ + "other_calling_module.ex" => [ + %{ + callee: {CalledModule, :other_called, 1}, + column: 3, + file: "other_calling_module.ex", + line: 13 + } + ] + }} + ] == sorted_calls() + end + + test "deletes calls by file" do + Tracer.trace( + {:remote_function, [line: 12, column: 2], CalledModule, :called, 1}, + %Macro.Env{ + module: CallingModule, + file: "calling_module.ex" + } + ) + + Tracer.trace( + {:remote_function, [line: 13, column: 3], CalledModule, :called, 1}, + %Macro.Env{ + module: OtherCallingModule, + file: "other_calling_module.ex" + } + ) + + Tracer.delete_calls_by_file("other_calling_module.ex") + + assert [ + {{CalledModule, :called, 1}, + %{ + "calling_module.ex" => [ + %{ + callee: {CalledModule, :called, 1}, + column: 2, + file: "calling_module.ex", + line: 12 + } + ] + }} + ] == sorted_calls() + + Tracer.delete_calls_by_file("calling_module.ex") + + assert [] == sorted_calls() + end + end +end diff --git a/mix.lock b/mix.lock index 30f8eac22..869f92dd8 100644 --- a/mix.lock +++ b/mix.lock @@ -1,7 +1,7 @@ %{ "dialyxir": {:hex, :dialyxir, "1.1.0", "c5aab0d6e71e5522e77beff7ba9e08f8e02bad90dfbeffae60eaf0cb47e29488", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "07ea8e49c45f15264ebe6d5b93799d4dd56a44036cf42d0ad9c960bc266c0b9a"}, "docsh": {:hex, :docsh, "0.7.2", "f893d5317a0e14269dd7fe79cf95fb6b9ba23513da0480ec6e77c73221cae4f2", [:rebar3], [{:providers, "1.8.1", [hex: :providers, repo: "hexpm", optional: false]}], "hexpm", "4e7db461bb07540d2bc3d366b8513f0197712d0495bb85744f367d3815076134"}, - "elixir_sense": {:git, "https://github.com/elixir-lsp/elixir_sense.git", "08bac2a5a0ad867908d77a7087eac756bf7cce43", []}, + "elixir_sense": {:git, "https://github.com/elixir-lsp/elixir_sense.git", "12bdf7ac9971a9f1cb278b66364f912d63af4c0f", []}, "erl2ex": {:git, "https://github.com/dazuma/erl2ex.git", "244c2d9ed5805ef4855a491d8616b8842fef7ca4", []}, "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, "getopt": {:hex, :getopt, "1.0.1", "c73a9fa687b217f2ff79f68a3b637711bb1936e712b521d8ce466b29cbf7808a", [:rebar3], [], "hexpm", "53e1ab83b9ceb65c9672d3e7a35b8092e9bdc9b3ee80721471a161c10c59959c"}, From 65e889a65420d06133acc2e8d2085477b840490b Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Thu, 29 Sep 2022 09:29:11 +0200 Subject: [PATCH 057/125] Do not crash on completions in untitled: schema Fixes https://github.com/elixir-lsp/elixir-ls/issues/732 --- .../lib/language_server/server.ex | 8 +++++- .../test/providers/completion_test.exs | 28 +++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/apps/language_server/lib/language_server/server.ex b/apps/language_server/lib/language_server/server.ex index d5fef5c7d..f052c88c5 100644 --- a/apps/language_server/lib/language_server/server.ex +++ b/apps/language_server/lib/language_server/server.ex @@ -695,6 +695,12 @@ defmodule ElixirLS.LanguageServer.Server do signature_after_complete = Map.get(state.settings || %{}, "signatureAfterComplete", true) + path = + case uri do + "file:" <> _ -> SourceFile.path_from_uri(uri) + _ -> nil + end + fun = fn -> Completion.completion(source_file.text, line, character, snippets_supported: snippets_supported, @@ -703,7 +709,7 @@ defmodule ElixirLS.LanguageServer.Server do signature_help_supported: signature_help_supported, locals_without_parens: locals_without_parens, signature_after_complete: signature_after_complete, - file_path: SourceFile.path_from_uri(uri) + file_path: path ) end diff --git a/apps/language_server/test/providers/completion_test.exs b/apps/language_server/test/providers/completion_test.exs index b15069455..b14afd083 100644 --- a/apps/language_server/test/providers/completion_test.exs +++ b/apps/language_server/test/providers/completion_test.exs @@ -962,6 +962,34 @@ defmodule ElixirLS.LanguageServer.Providers.CompletionTest do "insertText" => "defmodule $1 do\n\t$0\nend" } = first end + + test "will suggest defmodule without module_name snippet when file path is nil" do + text = """ + defmod + # ^ + """ + + {line, char} = {0, 6} + + TestUtils.assert_has_cursor_char(text, line, char) + + assert {:ok, %{"items" => [first | _] = _items}} = + Completion.completion( + text, + line, + char, + @supports + |> Keyword.put( + :file_path, + nil + ) + ) + + assert %{ + "label" => "defmodule", + "insertText" => "defmodule $1 do\n\t$0\nend" + } = first + end end describe "generic suggestions" do From 1cf22eee7f126dddedbed55786a6dc63b57d49d8 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Thu, 29 Sep 2022 21:14:25 +0200 Subject: [PATCH 058/125] allow formatting of untitled: even when project_dir is set Fixes https://github.com/elixir-lsp/elixir-ls/issues/691 --- .../lib/language_server/providers/formatting.ex | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/language_server/lib/language_server/providers/formatting.ex b/apps/language_server/lib/language_server/providers/formatting.ex index a0edd0a11..a30fceb55 100644 --- a/apps/language_server/lib/language_server/providers/formatting.ex +++ b/apps/language_server/lib/language_server/providers/formatting.ex @@ -3,7 +3,8 @@ defmodule ElixirLS.LanguageServer.Providers.Formatting do alias ElixirLS.LanguageServer.Protocol.TextEdit alias ElixirLS.LanguageServer.SourceFile - def format(%SourceFile{} = source_file, uri, project_dir) when is_binary(project_dir) do + def format(%SourceFile{} = source_file, uri = "file:" <> _, project_dir) + when is_binary(project_dir) do if can_format?(uri, project_dir) do case SourceFile.formatter_for(uri) do {:ok, {formatter, opts}} -> @@ -32,7 +33,8 @@ defmodule ElixirLS.LanguageServer.Providers.Formatting do end end - def format(%SourceFile{} = source_file, _uri, nil) do + # if project_dir is not set or schema is not file: we format with default options + def format(%SourceFile{} = source_file, _uri, _project_dir) do do_format(source_file) end From a40581e9a38bbad84e532dc72b686fee37cab064 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Thu, 29 Sep 2022 21:26:31 +0200 Subject: [PATCH 059/125] Fix crash on hover when project_dir is nil Fixes https://github.com/elixir-lsp/elixir-ls/issues/719 --- apps/language_server/lib/language_server/providers/hover.ex | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/language_server/lib/language_server/providers/hover.ex b/apps/language_server/lib/language_server/providers/hover.ex index 041ed9095..c6491a0b2 100644 --- a/apps/language_server/lib/language_server/providers/hover.ex +++ b/apps/language_server/lib/language_server/providers/hover.ex @@ -90,7 +90,7 @@ defmodule ElixirLS.LanguageServer.Providers.Hover do cond do erlang_module?(subject) -> - # erlang moudle is not support now. + # TODO erlang module is currently not supported "" true -> @@ -162,6 +162,8 @@ defmodule ElixirLS.LanguageServer.Providers.Hover do ("Elixir." <> mod_name) |> String.to_atom() |> function_exported?(:__info__, 1) end + defp third_dep?(source, nil), do: false + defp third_dep?(source, project_dir) do prefix = project_dir <> "/deps" String.starts_with?(source, prefix) From 90752e832728af228d4e455ad2c09bd730a8048f Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sat, 1 Oct 2022 12:08:33 +0200 Subject: [PATCH 060/125] set compiler options in case project left some garbage there Fixes https://github.com/elixir-lsp/elixir-ls/issues/717 Workaround for https://github.com/elixir-lang/elixir/issues/12159 --- apps/language_server/lib/language_server/build.ex | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/apps/language_server/lib/language_server/build.ex b/apps/language_server/lib/language_server/build.ex index 622910dde..6365cd976 100644 --- a/apps/language_server/lib/language_server/build.ex +++ b/apps/language_server/lib/language_server/build.ex @@ -79,6 +79,11 @@ defmodule ElixirLS.LanguageServer.Build do # FIXME: Private API Mix.ProjectStack.post_config(build_path: ".elixir_ls/build") + # we need to reset compiler options + # project may leave tracers after previous compilation and we don't woant them interfeering + # see https://github.com/elixir-lsp/elixir-ls/issues/717 + set_compiler_options() + # We can get diagnostics if Mixfile fails to load {status, diagnostics} = case Kernel.ParallelCompiler.compile([mixfile]) do From dacdcdf47b8feec87d187302d1a880bcd6f54d25 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sat, 1 Oct 2022 21:03:03 +0200 Subject: [PATCH 061/125] Do not crash when handling code lenses for untitled Fixes https://github.com/elixir-lsp/elixir-ls/issues/734 --- apps/language_server/lib/language_server/server.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/language_server/lib/language_server/server.ex b/apps/language_server/lib/language_server/server.ex index f052c88c5..f87003ff5 100644 --- a/apps/language_server/lib/language_server/server.ex +++ b/apps/language_server/lib/language_server/server.ex @@ -863,7 +863,7 @@ defmodule ElixirLS.LanguageServer.Server do defp get_test_code_lenses( state = %__MODULE__{project_dir: project_dir}, - uri, + "file:" <> _ = uri, source_file, true = _enabled, true = _umbrella @@ -888,7 +888,7 @@ defmodule ElixirLS.LanguageServer.Server do defp get_test_code_lenses( %__MODULE__{project_dir: project_dir}, - uri, + "file:" <> _ = uri, source_file, true = _enabled, false = _umbrella From 9fe8ea1f453e0c2c7f7d5bc3fb6da7d4e98bee9f Mon Sep 17 00:00:00 2001 From: Luca Cervello Date: Sun, 2 Oct 2022 23:11:57 +0200 Subject: [PATCH 062/125] Add basic code action support (#718) --- .../lib/language_server/protocol.ex | 11 ++++ .../language_server/providers/code_action.ex | 61 ++++++++++++++++++ .../lib/language_server/server.ex | 14 +++- apps/language_server/test/server_test.exs | 64 +++++++++++++++++++ 4 files changed, 147 insertions(+), 3 deletions(-) create mode 100644 apps/language_server/lib/language_server/providers/code_action.ex diff --git a/apps/language_server/lib/language_server/protocol.ex b/apps/language_server/lib/language_server/protocol.ex index faf5af976..0845bfa4b 100644 --- a/apps/language_server/lib/language_server/protocol.ex +++ b/apps/language_server/lib/language_server/protocol.ex @@ -200,6 +200,17 @@ defmodule ElixirLS.LanguageServer.Protocol do end end + defmacro code_action_req(id, uri, diagnostics) do + quote do + request(unquote(id), "textDocument/codeAction", %{ + "context" => %{"diagnostics" => unquote(diagnostics)}, + "textDocument" => %{ + "uri" => unquote(uri) + } + }) + end + end + # Other utilities defmacro range(start_line, start_character, end_line, end_character) do diff --git a/apps/language_server/lib/language_server/providers/code_action.ex b/apps/language_server/lib/language_server/providers/code_action.ex new file mode 100644 index 000000000..9be0474a1 --- /dev/null +++ b/apps/language_server/lib/language_server/providers/code_action.ex @@ -0,0 +1,61 @@ +defmodule ElixirLS.LanguageServer.Providers.CodeAction do + use ElixirLS.LanguageServer.Protocol + + def code_actions(uri, diagnostics) do + actions = + diagnostics + |> Enum.map(fn diagnostic -> actions(uri, diagnostic) end) + |> List.flatten() + + {:ok, actions} + end + + defp actions(uri, %{"message" => message, "range" => range} = diagnostic) do + [ + {~r/variable "(.*)" is unused/, &prefix_with_underscore/2}, + {~r/variable "(.*)" is unused/, &remove_variable/2} + ] + |> Enum.filter(fn {r, _fun} -> String.match?(message, r) end) + |> Enum.map(fn {_r, fun} -> fun.(uri, diagnostic) end) + end + + defp prefix_with_underscore(uri, %{"message" => message, "range" => range} = diagnostic) do + %{ + "title" => "Add '_' to unused variable", + "kind" => "quickfix", + "edit" => %{ + "changes" => %{ + uri => [ + %{ + "newText" => "_", + "range" => + range( + range["start"]["line"], + range["start"]["character"], + range["start"]["line"], + range["start"]["character"] + ) + } + ] + } + } + } + end + + defp remove_variable(uri, %{"range" => range} = diagnostic) do + %{ + "title" => "Remove unused variable", + "kind" => "quickfix", + "edit" => %{ + "changes" => %{ + uri => [ + %{ + "newText" => "", + "range" => range + } + ] + } + } + } + end +end diff --git a/apps/language_server/lib/language_server/server.ex b/apps/language_server/lib/language_server/server.ex index f87003ff5..d70f12eba 100644 --- a/apps/language_server/lib/language_server/server.ex +++ b/apps/language_server/lib/language_server/server.ex @@ -31,7 +31,8 @@ defmodule ElixirLS.LanguageServer.Server do OnTypeFormatting, CodeLens, ExecuteCommand, - FoldingRange + FoldingRange, + CodeAction } alias ElixirLS.Utils.Launch @@ -326,7 +327,9 @@ defmodule ElixirLS.LanguageServer.Server do # close notification send before JsonRpc.log_message( :warning, - "Received textDocument/didOpen for file that is already open. Received uri: #{inspect(uri)}" + "Received textDocument/didOpen for file that is already open. Received uri: #{ + inspect(uri) + }" ) state @@ -788,6 +791,10 @@ defmodule ElixirLS.LanguageServer.Server do end end + defp handle_request(code_action_req(id, uri, diagnostics) = req, state = %__MODULE__{}) do + {:async, fn -> CodeAction.code_actions(uri, diagnostics) end, state} + end + defp handle_request(%{"method" => "$/" <> _}, state = %__MODULE__{}) do # "$/" requests that the server doesn't support must return method_not_found {:error, :method_not_found, nil, state} @@ -839,7 +846,8 @@ defmodule ElixirLS.LanguageServer.Server do "workspace" => %{ "workspaceFolders" => %{"supported" => false, "changeNotifications" => false} }, - "foldingRangeProvider" => true + "foldingRangeProvider" => true, + "codeActionProvider" => true } end diff --git a/apps/language_server/test/server_test.exs b/apps/language_server/test/server_test.exs index e3d5284c4..9d40135a6 100644 --- a/apps/language_server/test/server_test.exs +++ b/apps/language_server/test/server_test.exs @@ -1494,6 +1494,70 @@ defmodule ElixirLS.LanguageServer.ServerTest do end end + describe "textDocument/codeAction" do + test "return code actions on unused variables", %{server: server} do + uri = "file:///file.ex" + fake_initialize(server) + + Server.receive_packet(server, did_open(uri, "elixir", 1, "")) + + Server.receive_packet( + server, + code_action_req(1, uri, [ + %{ + "message" => + "variable \"foo\" is unused (if the variable is not meant to be used, prefix it with an underscore)", + "range" => %{ + "end" => %{"character" => 13, "line" => 19}, + "start" => %{"character" => 4, "line" => 19} + }, + "severity" => 1, + "source" => "Elixir" + } + ]) + ) + + resp = assert_receive(%{"id" => 1}, 5000) + + assert response(1, [ + %{ + "edit" => %{ + "changes" => %{ + "file:///file.ex" => [ + %{ + "newText" => "_", + "range" => %{ + "end" => %{"character" => 4, "line" => 19}, + "start" => %{"character" => 4, "line" => 19} + } + } + ] + } + }, + "kind" => "quickfix", + "title" => "Add '_' to unused variable" + }, + %{ + "edit" => %{ + "changes" => %{ + "file:///file.ex" => [ + %{ + "newText" => "", + "range" => %{ + "end" => %{"character" => 13, "line" => 19}, + "start" => %{"character" => 4, "line" => 19} + } + } + ] + } + }, + "kind" => "quickfix", + "title" => "Remove unused variable" + } + ]) == resp + end + end + defp with_new_server(func) do server = start_supervised!({Server, nil}) packet_capture = start_supervised!({PacketCapture, self()}) From 8ce39f345bac02b5e2f9a0960ba194134a4704d4 Mon Sep 17 00:00:00 2001 From: Thanabodee Charoenpiriyakij Date: Mon, 3 Oct 2022 13:25:09 +0700 Subject: [PATCH 063/125] Fix unused warning during compile (#735) Fixes issues below: ``` warning: variable "source" is unused (if the variable is not meant to be used, prefix it with an underscore) lib/language_server/providers/hover.ex:165: ElixirLS.LanguageServer.Providers.Hover.third_dep?/2 warning: variable "range" is unused (if the variable is not meant to be used, prefix it with an underscore) lib/language_server/providers/code_action.ex:13: ElixirLS.LanguageServer.Providers.CodeAction.actions/2 warning: variable "diagnostic" is unused (if the variable is not meant to be used, prefix it with an underscore) lib/language_server/providers/code_action.ex:22: ElixirLS.LanguageServer.Providers.CodeAction.prefix_with_underscore/2 warning: variable "message" is unused (if the variable is not meant to be used, prefix it with an underscore) lib/language_server/providers/code_action.ex:22: ElixirLS.LanguageServer.Providers.CodeAction.prefix_with_underscore/2 warning: variable "diagnostic" is unused (if the variable is not meant to be used, prefix it with an underscore) lib/language_server/providers/code_action.ex:45: ElixirLS.LanguageServer.Providers.CodeAction.remove_variable/2 warning: variable "id" is unused (if the variable is not meant to be used, prefix it with an underscore) lib/language_server/server.ex:794: ElixirLS.LanguageServer.Server.handle_request/2 warning: variable "req" is unused (if the variable is not meant to be used, prefix it with an underscore) lib/language_server/server.ex:794: ElixirLS.LanguageServer.Server.handle_request/2 ``` --- .../lib/language_server/providers/code_action.ex | 6 +++--- apps/language_server/lib/language_server/providers/hover.ex | 2 +- apps/language_server/lib/language_server/server.ex | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/language_server/lib/language_server/providers/code_action.ex b/apps/language_server/lib/language_server/providers/code_action.ex index 9be0474a1..288b9bfd3 100644 --- a/apps/language_server/lib/language_server/providers/code_action.ex +++ b/apps/language_server/lib/language_server/providers/code_action.ex @@ -10,7 +10,7 @@ defmodule ElixirLS.LanguageServer.Providers.CodeAction do {:ok, actions} end - defp actions(uri, %{"message" => message, "range" => range} = diagnostic) do + defp actions(uri, %{"message" => message} = diagnostic) do [ {~r/variable "(.*)" is unused/, &prefix_with_underscore/2}, {~r/variable "(.*)" is unused/, &remove_variable/2} @@ -19,7 +19,7 @@ defmodule ElixirLS.LanguageServer.Providers.CodeAction do |> Enum.map(fn {_r, fun} -> fun.(uri, diagnostic) end) end - defp prefix_with_underscore(uri, %{"message" => message, "range" => range} = diagnostic) do + defp prefix_with_underscore(uri, %{"range" => range}) do %{ "title" => "Add '_' to unused variable", "kind" => "quickfix", @@ -42,7 +42,7 @@ defmodule ElixirLS.LanguageServer.Providers.CodeAction do } end - defp remove_variable(uri, %{"range" => range} = diagnostic) do + defp remove_variable(uri, %{"range" => range}) do %{ "title" => "Remove unused variable", "kind" => "quickfix", diff --git a/apps/language_server/lib/language_server/providers/hover.ex b/apps/language_server/lib/language_server/providers/hover.ex index c6491a0b2..dd18393f1 100644 --- a/apps/language_server/lib/language_server/providers/hover.ex +++ b/apps/language_server/lib/language_server/providers/hover.ex @@ -162,7 +162,7 @@ defmodule ElixirLS.LanguageServer.Providers.Hover do ("Elixir." <> mod_name) |> String.to_atom() |> function_exported?(:__info__, 1) end - defp third_dep?(source, nil), do: false + defp third_dep?(_source, nil), do: false defp third_dep?(source, project_dir) do prefix = project_dir <> "/deps" diff --git a/apps/language_server/lib/language_server/server.ex b/apps/language_server/lib/language_server/server.ex index d70f12eba..7a62754f8 100644 --- a/apps/language_server/lib/language_server/server.ex +++ b/apps/language_server/lib/language_server/server.ex @@ -791,7 +791,7 @@ defmodule ElixirLS.LanguageServer.Server do end end - defp handle_request(code_action_req(id, uri, diagnostics) = req, state = %__MODULE__{}) do + defp handle_request(code_action_req(_id, uri, diagnostics), state = %__MODULE__{}) do {:async, fn -> CodeAction.code_actions(uri, diagnostics) end, state} end From 866601ef0420a67fdffeb081b13c5df9dae7ff72 Mon Sep 17 00:00:00 2001 From: Milo Lee Date: Mon, 3 Oct 2022 20:30:58 +0800 Subject: [PATCH 064/125] Suggest an appropriate module name with the 'defprotocol' snippet (#715) --- .../language_server/providers/completion.ex | 5 ++ .../test/providers/completion_test.exs | 56 +++++++++++++++++++ 2 files changed, 61 insertions(+) diff --git a/apps/language_server/lib/language_server/providers/completion.ex b/apps/language_server/lib/language_server/providers/completion.ex index 202441bc3..2f06a9861 100644 --- a/apps/language_server/lib/language_server/providers/completion.ex +++ b/apps/language_server/lib/language_server/providers/completion.ex @@ -587,6 +587,11 @@ defmodule ElixirLS.LanguageServer.Providers.Completion do "defmodule #{suggest_module_name(file_path)}$1 do\n\t$0\nend" end + defp snippet_for({"Kernel", "defprotocol"}, %{file_path: file_path}) + when is_binary(file_path) do + "defprotocol #{suggest_module_name(file_path)}$1 do\n\t$0\nend" + end + defp snippet_for(key, %{pipe_before?: true}) do # Get pipe-friendly version of snippet if available, otherwise fallback to standard Map.get(@pipe_func_snippets, key) || Map.get(@func_snippets, key) diff --git a/apps/language_server/test/providers/completion_test.exs b/apps/language_server/test/providers/completion_test.exs index b14afd083..941b32242 100644 --- a/apps/language_server/test/providers/completion_test.exs +++ b/apps/language_server/test/providers/completion_test.exs @@ -990,6 +990,62 @@ defmodule ElixirLS.LanguageServer.Providers.CompletionTest do "insertText" => "defmodule $1 do\n\t$0\nend" } = first end + + test "will suggest defprotocol with protocol_name snippet when file path matches **/lib/**/*.ex" do + text = """ + defpro + # ^ + """ + + {line, char} = {0, 6} + + TestUtils.assert_has_cursor_char(text, line, char) + + assert {:ok, %{"items" => [first | _] = _items}} = + Completion.completion( + text, + line, + char, + @supports + |> Keyword.put( + :file_path, + "/some/path/my_project/lib/my_project/sub_folder/my_file.ex" + ) + ) + + assert %{ + "label" => "defprotocol", + "insertText" => "defprotocol MyProject.SubFolder.MyFile$1 do\n\t$0\nend" + } = first + end + + test "will suggest defprotocol without protocol_name snippet when file path does not match expected patterns" do + text = """ + defpro + # ^ + """ + + {line, char} = {0, 6} + + TestUtils.assert_has_cursor_char(text, line, char) + + assert {:ok, %{"items" => [first | _] = _items}} = + Completion.completion( + text, + line, + char, + @supports + |> Keyword.put( + :file_path, + "/some/path/my_project/lib/my_project/sub_folder/my_file.heex" + ) + ) + + assert %{ + "label" => "defprotocol", + "insertText" => "defprotocol $1 do\n\t$0\nend" + } = first + end end describe "generic suggestions" do From b9414214c03cfad2d54b2180af8aa6a2b47d0758 Mon Sep 17 00:00:00 2001 From: George Rodrigues Date: Tue, 4 Oct 2022 11:28:14 -0300 Subject: [PATCH 065/125] docs: fix typos (#737) --- CHANGELOG.md | 6 +++--- DEVELOPMENT.md | 2 +- README.md | 8 ++++---- apps/elixir_ls_debugger/lib/debugger/binding.ex | 2 +- .../language_server/test/support/fixtures/example_docs.ex | 2 +- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b891c20a..363bb44f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,7 @@ Improvements: - Debugger completions now return detal field - Diagnostic positions now return column position returned by compiler (elixir 1.14+) - Diagnostic position fixed to never return invalid negative values -- An exat `do` keyword completion is now preselected and more preferred over `defoverridable` +- An exact `do` keyword completion is now preselected and more preferred over `defoverridable` - Fixed hexdoc links in hover for aliased modules and imported functions ([Milo Lee](https://github.com/oo6)) - Better module name suggestions in Phoenix `live` directory ([Manos Emmanouilidis](https://github.com/bottlenecked)) @@ -29,7 +29,7 @@ Improvements to debugger addapter: - Messages in the queue of debugged process can now be examined [#681](https://github.com/elixir-lsp/elixir-ls/pull/681) (thanks [Łukasz Samson](https://github.com/lukaszsamson)) - Debugger can now handle pause and terminateThread requests [#675](https://github.com/elixir-lsp/elixir-ls/pull/675) (thanks [Łukasz Samson](https://github.com/lukaszsamson)) - Clipboard and hover eval is now supported in debugger [#680](https://github.com/elixir-lsp/elixir-ls/pull/680) (thanks [Łukasz Samson](https://github.com/lukaszsamson)) -- Auto interpretting can now be disabled [#616](https://github.com/elixir-lsp/elixir-ls/pull/616) (thanks [Jason Axelson](https://github.com/axelson)) +- Auto interpreting can now be disabled [#616](https://github.com/elixir-lsp/elixir-ls/pull/616) (thanks [Jason Axelson](https://github.com/axelson)) - Debugger conforms better to DAP 1.51 specification [#678](https://github.com/elixir-lsp/elixir-ls/pull/678) (thanks [Łukasz Samson](https://github.com/lukaszsamson)) Improvements to language server: @@ -63,7 +63,7 @@ VSCode: - Improved regex with < highlighting [#226](https://github.com/elixir-lsp/vscode-elixir-ls/pull/226) (thanks [Tiago Moraes](https://github.com/tiagoefmoraes)) - Extension updated to use LSP v3.16 [#227](https://github.com/elixir-lsp/vscode-elixir-ls/pull/227) (thanks [Łukasz Samson](https://github.com/lukaszsamson)) -Houskeeping: +Housekeeping: thanks [Łukasz Samson](https://github.com/lukaszsamson), [Thanabodee Charoenpiriyakij](https://github.com/wingyplus), [Daniils Petrovs](https://github.com/DaniruKun), [Jason Axelson](https://github.com/axelson) diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 8b71ddb3f..470911a01 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -12,7 +12,7 @@ ElixirLS generally aims to support the last 3 versions of Elixir and the last 3 ## Packaging -Follow those instuctions when publishing a new release. +Follow those instructions when publishing a new release. 1. Bump the changelog 2. Bump the version numbers in `apps/elixir_ls_debugger/mix.exs`, `apps/elixir_ls_utils/mix.exs`, and `apps/language_server/mix.exs` diff --git a/README.md b/README.md index 7c6f1f3e8..d1354944b 100644 --- a/README.md +++ b/README.md @@ -104,7 +104,7 @@ When debugging in Elixir or Erlang, only modules that have been "interpreted" (u Currently there is a limit of 100 breakpoints. -### Debuging tests and `.exs` files +### Debugging tests and `.exs` files In order to debug modules in `.exs` files (such as tests), they must be specified under `requireFiles` in your launch configuration so they can be loaded and interpreted prior to running the task. For example, the default launch configuration for "mix test" in the VS Code plugin looks like this: @@ -193,15 +193,15 @@ Break conditions are supported and evaluate elixir expressions within the contex ### Hit conditions -An expression that evaluates to integer can be used to contro how many hits of a breakpoint are ignored before the process is stopped. +An expression that evaluates to integer can be used to control how many hits of a breakpoint are ignored before the process is stopped. ### Log points -When log message is set on a breakpoint the debugger will not break but instead log a message to standard output (as required by Debug Adapter Protocol specification). The message may contain interpolated expressions in `{}`, e.g. `my_var is {inspect(my_var)}` and will be evaluated in the context of the process. Special characters `{` and `}` can be emited with escape sequence `\{` and `\}`. As of Debug Adapter Protocol specification version 1.51, log messages are not supported on function breakpoints. +When log message is set on a breakpoint the debugger will not break but instead log a message to standard output (as required by Debug Adapter Protocol specification). The message may contain interpolated expressions in `{}`, e.g. `my_var is {inspect(my_var)}` and will be evaluated in the context of the process. Special characters `{` and `}` can be emitted with escape sequence `\{` and `\}`. As of Debug Adapter Protocol specification version 1.51, log messages are not supported on function breakpoints. ### Expression evaluator -An expression evaluator is included in the debbuger. It evaluates elixir expressions in the context of a process stopped on a breakpoint. All bound variables are accessible (no support for attributes as those are compile time). Please note that there are limitations due to `:int` operating on beam instruction level. The binding returns multiple versions of variables in Static Singe Assignment with no indication which one is valid in the current elixir scope. A heuristic is used that selects the highest versions but it does not behave correctly in all cases, e.g. in +An expression evaluator is included in the debugger. It evaluates elixir expressions in the context of a process stopped on a breakpoint. All bound variables are accessible (no support for attributes as those are compile time). Please note that there are limitations due to `:int` operating on beam instruction level. The binding returns multiple versions of variables in Static Singe Assignment with no indication which one is valid in the current elixir scope. A heuristic is used that selects the highest versions but it does not behave correctly in all cases, e.g. in ```elixir a = 4 diff --git a/apps/elixir_ls_debugger/lib/debugger/binding.ex b/apps/elixir_ls_debugger/lib/debugger/binding.ex index 7bf9b916c..846541b47 100644 --- a/apps/elixir_ls_debugger/lib/debugger/binding.ex +++ b/apps/elixir_ls_debugger/lib/debugger/binding.ex @@ -8,7 +8,7 @@ defmodule ElixirLS.Debugger.Binding do end) |> Enum.map(fn {classic_key, list} -> # assume binding with highest number is the current one - # this may not be allways true, e.g. in + # this may not be always true, e.g. in # a = 5 # if true do # a = 4 diff --git a/apps/language_server/test/support/fixtures/example_docs.ex b/apps/language_server/test/support/fixtures/example_docs.ex index b551b42b9..1c0a74bec 100644 --- a/apps/language_server/test/support/fixtures/example_docs.ex +++ b/apps/language_server/test/support/fixtures/example_docs.ex @@ -2,7 +2,7 @@ defmodule ElixirLS.LanguageServer.Fixtures.ExampleDocs do @doc """ The summary - Ths rest + This rest """ @spec add(a_big_name :: integer, b_big_name :: integer) :: integer def add(a, b) do From 036d876484b8a8265cd93af7a14c729a6089af5e Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Thu, 6 Oct 2022 09:08:25 +0200 Subject: [PATCH 066/125] Deprioretize deprecated completions https://github.com/elixir-lsp/elixir-ls/issues/682 --- .../lib/language_server/providers/completion.ex | 10 +++++++++- apps/language_server/lib/language_server/server.ex | 4 +--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/apps/language_server/lib/language_server/providers/completion.ex b/apps/language_server/lib/language_server/providers/completion.ex index 2f06a9861..295177070 100644 --- a/apps/language_server/lib/language_server/providers/completion.ex +++ b/apps/language_server/lib/language_server/providers/completion.ex @@ -954,7 +954,15 @@ defmodule ElixirLS.LanguageServer.Providers.Completion do end defp sort_items(items) do - Enum.sort_by(items, fn %__MODULE__{priority: priority, label: label} -> + Enum.sort_by(items, fn %__MODULE__{priority: priority, label: label} = item -> + # deprioretize deprecated + priority = + if item.tags |> Enum.any?(&(&1 == :deprecated)) do + priority + 30 + else + priority + end + {priority, label =~ Regex.recompile!(~r/^[^a-zA-Z0-9]/), label} end) end diff --git a/apps/language_server/lib/language_server/server.ex b/apps/language_server/lib/language_server/server.ex index 7a62754f8..98c00fd29 100644 --- a/apps/language_server/lib/language_server/server.ex +++ b/apps/language_server/lib/language_server/server.ex @@ -327,9 +327,7 @@ defmodule ElixirLS.LanguageServer.Server do # close notification send before JsonRpc.log_message( :warning, - "Received textDocument/didOpen for file that is already open. Received uri: #{ - inspect(uri) - }" + "Received textDocument/didOpen for file that is already open. Received uri: #{inspect(uri)}" ) state From 022d59e6be97fcbd54de0507daccb3d61c365b6f Mon Sep 17 00:00:00 2001 From: Steve Cohen Date: Thu, 6 Oct 2022 00:32:09 -0700 Subject: [PATCH 067/125] Improved DocumentSymbols resilience (#736) When editing a new file, it's common to type `defmodule`, but the server would crash on this and emit lots of warnings. This change makes it so the document symbols provider returns no symbols if that happens. --- .../providers/document_symbols.ex | 7 +++++++ .../test/providers/document_symbols_test.exs | 20 +++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/apps/language_server/lib/language_server/providers/document_symbols.ex b/apps/language_server/lib/language_server/providers/document_symbols.ex index 0a8790e21..ec8b47912 100644 --- a/apps/language_server/lib/language_server/providers/document_symbols.ex +++ b/apps/language_server/lib/language_server/providers/document_symbols.ex @@ -60,6 +60,12 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbols do ast |> Enum.map(&extract_modules(&1)) |> List.flatten() end + # handle a bare defimpl, defprotocol or defmodule + defp extract_modules({defname, _, nil} = ast) + when defname in [:defmodule, :defprotocol, :defimpl] do + [] + end + defp extract_modules({defname, _, _child_ast} = ast) when defname in [:defmodule, :defprotocol, :defimpl] do [extract_symbol("", ast)] @@ -72,6 +78,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbols do defp extract_modules(_ast), do: [] # Modules, protocols + defp extract_symbol(_module_name, {defname, location, arguments}) when defname in [:defmodule, :defprotocol] do {module_name, module_name_location, module_body} = diff --git a/apps/language_server/test/providers/document_symbols_test.exs b/apps/language_server/test/providers/document_symbols_test.exs index 1b6c3d5dc..e7b235a1c 100644 --- a/apps/language_server/test/providers/document_symbols_test.exs +++ b/apps/language_server/test/providers/document_symbols_test.exs @@ -2474,4 +2474,24 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do } ]} = DocumentSymbols.symbols(uri, text, true) end + + describe "invalid documents" do + test "handles a module being defined" do + uri = "file:///project.test.ex" + text = "defmodule " + assert {:ok, []} = DocumentSymbols.symbols(uri, text, true) + end + + test "handles a protocol being defined" do + uri = "file:///project.test.ex" + text = "defprotocol " + assert {:ok, []} = DocumentSymbols.symbols(uri, text, true) + end + + test "handles a protocol being impolemented" do + uri = "file:///project.test.ex" + text = "defimpl " + assert {:ok, []} = DocumentSymbols.symbols(uri, text, true) + end + end end From 17ee36119142d14fdbd3af919bd005ccbb0c90b4 Mon Sep 17 00:00:00 2001 From: Steve Cohen Date: Thu, 6 Oct 2022 01:18:13 -0700 Subject: [PATCH 068/125] Improved resiliency of `formatter_for` (#738) It's possible that the call to `Mix.Tasks.Format.formatter_for_file` can cause a compile or syntax errors. Because these types of errors don't have a `message` key, the attempt to log them will fail, and the server will crash. This PR uses `Exception.message` to build the message text, which should work on any exceptions. --- .../lib/language_server/source_file.ex | 3 +- apps/language_server/mix.exs | 3 +- .../test/source_file/invalid_project_test.exs | 32 +++++++++++++++++++ mix.lock | 1 + 4 files changed, 37 insertions(+), 2 deletions(-) create mode 100644 apps/language_server/test/source_file/invalid_project_test.exs diff --git a/apps/language_server/lib/language_server/source_file.ex b/apps/language_server/lib/language_server/source_file.ex index 0ded9737a..c3a0d934c 100644 --- a/apps/language_server/lib/language_server/source_file.ex +++ b/apps/language_server/lib/language_server/source_file.ex @@ -348,8 +348,9 @@ defmodule ElixirLS.LanguageServer.SourceFile do end rescue e -> + message = Exception.message(e) IO.warn( - "Unable to get formatter options for #{path}: #{inspect(e.__struct__)} #{e.message}" + "Unable to get formatter options for #{path}: #{inspect(e.__struct__)} #{message}" ) :error diff --git a/apps/language_server/mix.exs b/apps/language_server/mix.exs index 0bc811424..57dd80f2f 100644 --- a/apps/language_server/mix.exs +++ b/apps/language_server/mix.exs @@ -32,7 +32,8 @@ defmodule ElixirLS.LanguageServer.Mixfile do {:dialyxir, "~> 1.0", runtime: false}, {:jason_vendored, github: "elixir-lsp/jason", branch: "vendored"}, {:stream_data, "~> 0.5", only: :test}, - {:path_glob_vendored, github: "elixir-lsp/path_glob", branch: "vendored"} + {:path_glob_vendored, github: "elixir-lsp/path_glob", branch: "vendored"}, + {:patch, "~> 0.12.0", only: :test} ] end diff --git a/apps/language_server/test/source_file/invalid_project_test.exs b/apps/language_server/test/source_file/invalid_project_test.exs new file mode 100644 index 000000000..a4ed16635 --- /dev/null +++ b/apps/language_server/test/source_file/invalid_project_test.exs @@ -0,0 +1,32 @@ +defmodule ElixirLS.LanguageServer.SourceFile.InvalidProjectTest do + + use ExUnit.Case, async: false + + use Patch + alias ElixirLS.LanguageServer.SourceFile + import ExUnit.CaptureIO + + describe "formatter_for " do + + test "should handle syntax errors" do + patch(Mix.Tasks.Format, :formatter_for_file, fn _ -> raise %SyntaxError{file: ".formatter.exs", line: 1} end) + + output = capture_io(:stderr, fn -> + assert :error = SourceFile.formatter_for("file:///root.ex") + end) + + assert String.contains?(output, "Unable to get formatter options") + end + + test "should handle compile errors" do + patch(Mix.Tasks.Format, :formatter_for_file, fn _ -> raise %SyntaxError{file: ".formatter.exs", line: 1} end) + + output = capture_io(:stderr, fn -> + assert :error = SourceFile.formatter_for("file:///root.ex") + end) + + assert String.contains?(output, "Unable to get formatter options") + end + + end +end diff --git a/mix.lock b/mix.lock index 869f92dd8..4c0b07c5b 100644 --- a/mix.lock +++ b/mix.lock @@ -8,6 +8,7 @@ "jason_vendored": {:git, "https://github.com/elixir-lsp/jason.git", "ee95ca80cd67b3a499a14f469536140935eb4483", [branch: "vendored"]}, "mix_task_archive_deps": {:git, "https://github.com/elixir-lsp/mix_task_archive_deps.git", "30fa76221def649286835685fec5d151be83c354", []}, "nimble_parsec": {:hex, :nimble_parsec, "1.1.0", "3a6fca1550363552e54c216debb6a9e95bd8d32348938e13de5eda962c0d7f89", [:mix], [], "hexpm", "08eb32d66b706e913ff748f11694b17981c0b04a33ef470e33e11b3d3ac8f54b"}, + "patch": {:hex, :patch, "0.12.0", "2da8967d382bade20344a3e89d618bfba563b12d4ac93955468e830777f816b0", [:mix], [], "hexpm", "ffd0e9a7f2ad5054f37af84067ee88b1ad337308a1cb227e181e3967127b0235"}, "path_glob_vendored": {:git, "https://github.com/elixir-lsp/path_glob.git", "965350dc41def7be4a70a23904195c733a2ecc84", [branch: "vendored"]}, "providers": {:hex, :providers, "1.8.1", "70b4197869514344a8a60e2b2a4ef41ca03def43cfb1712ecf076a0f3c62f083", [:rebar3], [{:getopt, "1.0.1", [hex: :getopt, repo: "hexpm", optional: false]}], "hexpm", "e45745ade9c476a9a469ea0840e418ab19360dc44f01a233304e118a44486ba0"}, "stream_data": {:hex, :stream_data, "0.5.0", "b27641e58941685c75b353577dc602c9d2c12292dd84babf506c2033cd97893e", [:mix], [], "hexpm", "012bd2eec069ada4db3411f9115ccafa38540a3c78c4c0349f151fc761b9e271"}, From 9160db59c7b0b7f0af26597bdf309147dcd4dea6 Mon Sep 17 00:00:00 2001 From: Hans Date: Thu, 6 Oct 2022 17:25:07 +0200 Subject: [PATCH 069/125] add support for disabling automatic builds trough a config (#440) --- apps/language_server/lib/language_server/server.ex | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/language_server/lib/language_server/server.ex b/apps/language_server/lib/language_server/server.ex index 98c00fd29..21998ed29 100644 --- a/apps/language_server/lib/language_server/server.ex +++ b/apps/language_server/lib/language_server/server.ex @@ -941,11 +941,12 @@ defmodule ElixirLS.LanguageServer.Server do # Build defp trigger_build(state = %__MODULE__{project_dir: project_dir}) do + build_automatically = Map.get(state.settings || %{}, "autoBuild", true) cond do not build_enabled?(state) -> state - not state.build_running? -> + not state.build_running? and build_automatically -> fetch_deps? = Map.get(state.settings || %{}, "fetchDeps", false) {_pid, build_ref} = Build.build(self(), project_dir, fetch_deps?: fetch_deps?) From d77087af403e5925a35216ebd54add98f98af0a5 Mon Sep 17 00:00:00 2001 From: Steve Cohen Date: Fri, 7 Oct 2022 00:22:44 -0700 Subject: [PATCH 070/125] Moved SourceFile path operations to SourceFile.Path module (#739) * Moved SourceFile path operations to SourceFile.Path module SourceFile is doing a lot of stuff that it doesn't need to do; the path operations are a good example of code that can be better when it's extracted from the SourceFile module. * fixed merge error --- .../lib/language_server/diagnostics.ex | 2 +- .../lib/language_server/protocol/location.ex | 2 +- .../providers/code_lens/test.ex | 2 +- .../language_server/providers/formatting.ex | 4 +- .../language_server/providers/references.ex | 2 +- .../providers/workspace_symbols.ex | 4 +- .../lib/language_server/server.ex | 29 ++- .../lib/language_server/source_file.ex | 104 +--------- .../lib/language_server/source_file/path.ex | 115 +++++++++++ apps/language_server/test/dialyzer_test.exs | 44 ++-- .../test/providers/definition_test.exs | 32 +-- .../execute_command/mix_clean_test.exs | 2 +- .../test/providers/formatting_test.exs | 22 +- .../test/providers/implementation_test.exs | 2 +- .../test/providers/references_test.exs | 12 +- .../test/providers/workspace_symbols_test.exs | 2 +- apps/language_server/test/server_test.exs | 50 +++-- .../test/source_file/path_test.exs | 191 ++++++++++++++++++ .../language_server/test/source_file_test.exs | 180 ----------------- 19 files changed, 410 insertions(+), 391 deletions(-) create mode 100644 apps/language_server/lib/language_server/source_file/path.ex create mode 100644 apps/language_server/test/source_file/path_test.exs diff --git a/apps/language_server/lib/language_server/diagnostics.ex b/apps/language_server/lib/language_server/diagnostics.ex index 63ada28e2..4da7954c7 100644 --- a/apps/language_server/lib/language_server/diagnostics.ex +++ b/apps/language_server/lib/language_server/diagnostics.ex @@ -185,7 +185,7 @@ defmodule ElixirLS.LanguageServer.Diagnostics do def publish_file_diagnostics(uri, all_diagnostics, source_file) do diagnostics = all_diagnostics - |> Enum.filter(&(SourceFile.path_to_uri(&1.file) == uri)) + |> Enum.filter(&(SourceFile.Path.to_uri(&1.file) == uri)) |> Enum.sort_by(fn %{position: position} -> position end) diagnostics_json = diff --git a/apps/language_server/lib/language_server/protocol/location.ex b/apps/language_server/lib/language_server/protocol/location.ex index 6e76f4a09..a8eaa2857 100644 --- a/apps/language_server/lib/language_server/protocol/location.ex +++ b/apps/language_server/lib/language_server/protocol/location.ex @@ -18,7 +18,7 @@ defmodule ElixirLS.LanguageServer.Protocol.Location do uri = case file do nil -> current_file_uri - _ -> SourceFile.path_to_uri(file) + _ -> SourceFile.Path.to_uri(file) end text = diff --git a/apps/language_server/lib/language_server/providers/code_lens/test.ex b/apps/language_server/lib/language_server/providers/code_lens/test.ex index 8b18e9499..bf3c445f8 100644 --- a/apps/language_server/lib/language_server/providers/code_lens/test.ex +++ b/apps/language_server/lib/language_server/providers/code_lens/test.ex @@ -20,7 +20,7 @@ defmodule ElixirLS.LanguageServer.Providers.CodeLens.Test do with {:ok, buffer_file_metadata} <- parse_source(text) do source_lines = SourceFile.lines(text) - file_path = SourceFile.path_from_uri(uri) + file_path = SourceFile.Path.from_uri(uri) calls_list = buffer_file_metadata.calls diff --git a/apps/language_server/lib/language_server/providers/formatting.ex b/apps/language_server/lib/language_server/providers/formatting.ex index a30fceb55..528956334 100644 --- a/apps/language_server/lib/language_server/providers/formatting.ex +++ b/apps/language_server/lib/language_server/providers/formatting.ex @@ -65,7 +65,7 @@ defmodule ElixirLS.LanguageServer.Providers.Formatting do # If in an umbrella project, the cwd might be set to a sub-app if it's being compiled. This is # fine if the file we're trying to format is in that app. Otherwise, we return an error. defp can_format?(file_uri = "file:" <> _, project_dir) do - file_path = file_uri |> SourceFile.abs_path_from_uri() + file_path = SourceFile.Path.absolute_from_uri(file_uri) String.starts_with?(file_path, Path.absname(project_dir)) or String.starts_with?(file_path, File.cwd!()) @@ -74,7 +74,7 @@ defmodule ElixirLS.LanguageServer.Providers.Formatting do defp can_format?(_uri, _project_dir), do: false defp should_format?(file_uri, project_dir, inputs) when is_list(inputs) do - file_path = file_uri |> SourceFile.abs_path_from_uri() + file_path = SourceFile.Path.absolute_from_uri(file_uri) formatter_dir = find_formatter_dir(project_dir, Path.dirname(file_path)) Enum.any?(inputs, fn input_glob -> diff --git a/apps/language_server/lib/language_server/providers/references.ex b/apps/language_server/lib/language_server/providers/references.ex index 2ca5ae513..adcf3d591 100644 --- a/apps/language_server/lib/language_server/providers/references.ex +++ b/apps/language_server/lib/language_server/providers/references.ex @@ -51,7 +51,7 @@ defmodule ElixirLS.LanguageServer.Providers.References do nil -> current_file_uri # ElixirSense returns a plain path (e.g. "/home/bob/my_app/lib/a.ex") as # the "uri" so we convert it to an actual uri - path when is_binary(path) -> SourceFile.path_to_uri(path) + path when is_binary(path) -> SourceFile.Path.to_uri(path) end end diff --git a/apps/language_server/lib/language_server/providers/workspace_symbols.ex b/apps/language_server/lib/language_server/providers/workspace_symbols.ex index 91a22ccf6..d2c399a66 100644 --- a/apps/language_server/lib/language_server/providers/workspace_symbols.ex +++ b/apps/language_server/lib/language_server/providers/workspace_symbols.ex @@ -156,7 +156,7 @@ defmodule ElixirLS.LanguageServer.Providers.WorkspaceSymbols do |> process_chunked(fn chunk -> for {module, beam_file} <- chunk, path = find_module_path(module, beam_file), - SourceFile.path_to_uri(path) in modified_uris, + SourceFile.Path.to_uri(path) in modified_uris, do: {module, path} end) @@ -487,7 +487,7 @@ defmodule ElixirLS.LanguageServer.Providers.WorkspaceSymbols do kind: @symbol_codes |> Map.fetch!(key), name: symbol_name(key, symbol), location: %{ - uri: SourceFile.path_to_uri(path), + uri: SourceFile.Path.to_uri(path), range: build_range(location) } } diff --git a/apps/language_server/lib/language_server/server.ex b/apps/language_server/lib/language_server/server.ex index 21998ed29..f81eadf3c 100644 --- a/apps/language_server/lib/language_server/server.ex +++ b/apps/language_server/lib/language_server/server.ex @@ -136,10 +136,7 @@ defmodule ElixirLS.LanguageServer.Server do def handle_call({:suggest_contracts, uri = "file:" <> _}, from, state = %__MODULE__{}) do case state do %{analysis_ready?: true, source_files: %{^uri => %{dirty?: false}}} -> - abs_path = - uri - |> SourceFile.abs_path_from_uri() - + abs_path = SourceFile.Path.absolute_from_uri(uri) {:reply, Dialyzer.suggest_contracts([abs_path]), state} %{source_files: %{^uri => _}} -> @@ -407,7 +404,7 @@ defmodule ElixirLS.LanguageServer.Server do needs_build = Enum.any?(changes, fn %{"uri" => uri = "file:" <> _, "type" => type} -> - path = SourceFile.path_from_uri(uri) + path = SourceFile.Path.from_uri(uri) relative_path = Path.relative_to(path, state.project_dir) first_path_segment = relative_path |> Path.split() |> hd @@ -423,7 +420,7 @@ defmodule ElixirLS.LanguageServer.Server do for change <- changes, change["type"] == 3, uniq: true, - do: SourceFile.path_from_uri(change["uri"]) + do: SourceFile.Path.from_uri(change["uri"]) for path <- deleted_paths do Tracer.notify_file_deleted(path) @@ -440,7 +437,7 @@ defmodule ElixirLS.LanguageServer.Server do # file created/updated - set dirty flag to false if file contents are equal case acc[uri] do %SourceFile{text: source_file_text, dirty?: true} = source_file -> - case File.read(SourceFile.path_from_uri(uri)) do + case File.read(SourceFile.Path.from_uri(uri)) do {:ok, ^source_file_text} -> Map.put(acc, uri, %SourceFile{source_file | dirty?: false}) @@ -536,9 +533,9 @@ defmodule ElixirLS.LanguageServer.Server do state = case root_uri do "file://" <> _ -> - root_path = SourceFile.abs_path_from_uri(root_uri) + root_path = SourceFile.Path.absolute_from_uri(root_uri) File.cd!(root_path) - cwd_uri = SourceFile.path_to_uri(File.cwd!()) + cwd_uri = SourceFile.Path.to_uri(File.cwd!()) %{state | root_uri: cwd_uri} nil -> @@ -698,7 +695,7 @@ defmodule ElixirLS.LanguageServer.Server do path = case uri do - "file:" <> _ -> SourceFile.path_from_uri(uri) + "file:" <> _ -> SourceFile.Path.from_uri(uri) _ -> nil end @@ -875,7 +872,7 @@ defmodule ElixirLS.LanguageServer.Server do true = _umbrella ) when is_binary(project_dir) do - file_path = SourceFile.path_from_uri(uri) + file_path = SourceFile.Path.from_uri(uri) Mix.Project.apps_paths() |> Enum.find(fn {_app, app_path} -> String.contains?(file_path, app_path) end) @@ -901,7 +898,7 @@ defmodule ElixirLS.LanguageServer.Server do ) when is_binary(project_dir) do try do - file_path = SourceFile.path_from_uri(uri) + file_path = SourceFile.Path.from_uri(uri) if is_test_file?(file_path) do CodeLens.test_code_lens(uri, source_file.text, project_dir) @@ -1038,14 +1035,14 @@ defmodule ElixirLS.LanguageServer.Server do contracts_by_file = not_dirty - |> Enum.map(fn {_from, uri} -> SourceFile.path_from_uri(uri) end) + |> Enum.map(fn {_from, uri} -> SourceFile.Path.from_uri(uri) end) |> Dialyzer.suggest_contracts() |> Enum.group_by(fn {file, _, _, _, _} -> file end) for {from, uri} <- not_dirty do contracts = contracts_by_file - |> Map.get(SourceFile.path_from_uri(uri), []) + |> Map.get(SourceFile.Path.from_uri(uri), []) GenServer.reply(from, contracts) end @@ -1083,7 +1080,7 @@ defmodule ElixirLS.LanguageServer.Server do Enum.uniq(Enum.map(new_diagnostics, & &1.file) ++ Enum.map(old_diagnostics, & &1.file)) for file <- files, - uri = SourceFile.path_to_uri(file), + uri = SourceFile.Path.to_uri(file), do: Diagnostics.publish_file_diagnostics( uri, @@ -1226,7 +1223,7 @@ defmodule ElixirLS.LanguageServer.Server do project_dir ) when is_binary(root_uri) do - root_dir = root_uri |> SourceFile.abs_path_from_uri() + root_dir = SourceFile.Path.absolute_from_uri(root_uri) project_dir = if is_binary(project_dir) do diff --git a/apps/language_server/lib/language_server/source_file.ex b/apps/language_server/lib/language_server/source_file.ex index c3a0d934c..ad0b14db0 100644 --- a/apps/language_server/lib/language_server/source_file.ex +++ b/apps/language_server/lib/language_server/source_file.ex @@ -62,108 +62,6 @@ defmodule ElixirLS.LanguageServer.SourceFile do apply_content_changes(source_file, rest) end - @doc """ - Returns path from URI in a way that handles windows file:///c%3A/... URLs correctly - """ - def path_from_uri(%URI{scheme: "file", path: path, authority: authority}) do - uri_path = - cond do - path == nil -> - # treat no path as root path - "/" - - authority not in ["", nil] and path not in ["", nil] -> - # UNC path - "//#{URI.decode(authority)}#{URI.decode(path)}" - - true -> - decoded_path = URI.decode(path) - - if match?({:win32, _}, :os.type()) and - String.match?(decoded_path, ~r/^\/[a-zA-Z]:/) do - # Windows drive letter path - # drop leading `/` and downcase drive letter - <<_, letter, path_rest::binary>> = decoded_path - <> - else - decoded_path - end - end - - case :os.type() do - {:win32, _} -> - # convert path separators from URI to Windows - String.replace(uri_path, ~r/\//, "\\") - - _ -> - uri_path - end - end - - def path_from_uri(%URI{scheme: scheme}) do - raise ArgumentError, message: "unexpected URI scheme #{inspect(scheme)}" - end - - def path_from_uri(uri) do - uri |> URI.parse() |> path_from_uri - end - - def path_to_uri(path) do - path = Path.expand(path) - - path = - case :os.type() do - {:win32, _} -> - # convert path separators from Windows to URI - String.replace(path, ~r/\\/, "/") - - _ -> - path - end - - {authority, path} = - case path do - "//" <> rest -> - # UNC path - extract authority - case String.split(rest, "/", parts: 2) do - [_] -> - # no path part, use root path - {rest, "/"} - - [a, ""] -> - # empty path part, use root path - {a, "/"} - - [a, p] -> - {a, "/" <> p} - end - - "/" <> _rest -> - {"", path} - - other -> - # treat as relative to root path - {"", "/" <> other} - end - - %URI{ - scheme: "file", - authority: authority |> URI.encode(), - # file system paths allow reserved URI characters that need to be escaped - # the exact rules are complicated but for simplicity we escape all reserved except `/` - # that's what https://github.com/microsoft/vscode-uri does - path: path |> URI.encode(&(&1 == ?/ or URI.char_unreserved?(&1))) - } - |> URI.to_string() - end - - defp downcase(char) when char >= ?A and char <= ?Z, do: char + 32 - defp downcase(char), do: char - - def abs_path_from_uri(uri) do - uri |> path_from_uri |> Path.absname() - end - def full_range(source_file) do lines = lines(source_file) @@ -336,7 +234,7 @@ defmodule ElixirLS.LanguageServer.SourceFile do @spec formatter_for(String.t()) :: {:ok, keyword()} | :error def formatter_for(uri = "file:" <> _) do - path = path_from_uri(uri) + path = __MODULE__.Path.from_uri(uri) try do true = Code.ensure_loaded?(Mix.Tasks.Format) diff --git a/apps/language_server/lib/language_server/source_file/path.ex b/apps/language_server/lib/language_server/source_file/path.ex new file mode 100644 index 000000000..31ffddddc --- /dev/null +++ b/apps/language_server/lib/language_server/source_file/path.ex @@ -0,0 +1,115 @@ +defmodule ElixirLS.LanguageServer.SourceFile.Path do + @file_scheme "file" + + @doc """ + Returns path from URI in a way that handles windows file:///c%3A/... URLs correctly + """ + def from_uri(%URI{scheme: @file_scheme, path: nil}) do + # treat no path as root path + convert_separators_to_native("/") + end + + def from_uri(%URI{scheme: @file_scheme, path: path, authority: authority}) + when path != "" and authority not in ["", nil] do + # UNC path + convert_separators_to_native("//#{URI.decode(authority)}#{URI.decode(path)}") + end + + def from_uri(%URI{scheme: @file_scheme, path: path}) do + decoded_path = URI.decode(path) + + if windows?() and String.match?(decoded_path, ~r/^\/[a-zA-Z]:/) do + # Windows drive letter path + # drop leading `/` and downcase drive letter + <<"/", letter::binary-size(1), path_rest::binary>> = decoded_path + "#{String.downcase(letter)}#{path_rest}" + else + decoded_path + end + |> convert_separators_to_native() + end + + def from_uri(%URI{scheme: scheme}) do + raise ArgumentError, message: "unexpected URI scheme #{inspect(scheme)}" + end + + def from_uri(uri) do + uri |> URI.parse() |> from_uri() + end + + def absolute_from_uri(uri) do + uri |> from_uri |> Path.absname() + end + + def to_uri(path) do + path = + path + |> Path.expand() + |> convert_separators_to_universal() + + {authority, path} = + case path do + "//" <> rest -> + # UNC path - extract authority + case String.split(rest, "/", parts: 2) do + [_] -> + # no path part, use root path + {rest, "/"} + + [authority, ""] -> + # empty path part, use root path + {authority, "/"} + + [authority, p] -> + {authority, "/" <> p} + end + + "/" <> _rest -> + {"", path} + + other -> + # treat as relative to root path + {"", "/" <> other} + end + + %URI{ + scheme: @file_scheme, + authority: authority |> URI.encode(), + # file system paths allow reserved URI characters that need to be escaped + # the exact rules are complicated but for simplicity we escape all reserved except `/` + # that's what https://github.com/microsoft/vscode-uri does + path: path |> URI.encode(&(&1 == ?/ or URI.char_unreserved?(&1))) + } + |> URI.to_string() + end + + defp convert_separators_to_native(path) do + if windows?() do + # convert path separators from URI to Windows + String.replace(path, ~r/\//, "\\") + else + path + end + end + + defp convert_separators_to_universal(path) do + if windows?() do + # convert path separators from Windows to URI + String.replace(path, ~r/\\/, "/") + else + path + end + end + + defp windows? do + case os_type() do + {:win32, _} -> true + _ -> false + end + end + + # this is here to be mocked in tests + defp os_type do + :os.type() + end +end diff --git a/apps/language_server/test/dialyzer_test.exs b/apps/language_server/test/dialyzer_test.exs index 2dfc944dd..58ce14d94 100644 --- a/apps/language_server/test/dialyzer_test.exs +++ b/apps/language_server/test/dialyzer_test.exs @@ -30,10 +30,10 @@ defmodule ElixirLS.LanguageServer.DialyzerTest do @tag slow: true, fixture: true test "reports diagnostics then clears them once problems are fixed", %{server: server} do in_fixture(__DIR__, "dialyzer", fn -> - file_a = SourceFile.path_to_uri(Path.absname("lib/a.ex")) + file_a = SourceFile.Path.to_uri(Path.absname("lib/a.ex")) capture_log(fn -> - root_uri = SourceFile.path_to_uri(File.cwd!()) + root_uri = SourceFile.Path.to_uri(File.cwd!()) Server.receive_packet(server, initialize_req(1, root_uri, %{})) Server.receive_packet( @@ -80,7 +80,7 @@ defmodule ElixirLS.LanguageServer.DialyzerTest do end """ - b_uri = SourceFile.path_to_uri("lib/b.ex") + b_uri = SourceFile.Path.to_uri("lib/b.ex") Server.receive_packet(server, did_open(b_uri, "elixir", 1, b_text)) Process.sleep(1500) File.write!("lib/b.ex", b_text) @@ -103,10 +103,10 @@ defmodule ElixirLS.LanguageServer.DialyzerTest do @tag slow: true, fixture: true test "only analyzes the changed files", %{server: server} do in_fixture(__DIR__, "dialyzer", fn -> - file_c = SourceFile.path_to_uri(Path.absname("lib/c.ex")) + file_c = SourceFile.Path.to_uri(Path.absname("lib/c.ex")) capture_log(fn -> - root_uri = SourceFile.path_to_uri(File.cwd!()) + root_uri = SourceFile.Path.to_uri(File.cwd!()) Server.receive_packet(server, initialize_req(1, root_uri, %{})) Server.receive_packet( @@ -123,7 +123,7 @@ defmodule ElixirLS.LanguageServer.DialyzerTest do end """ - c_uri = SourceFile.path_to_uri("lib/c.ex") + c_uri = SourceFile.Path.to_uri("lib/c.ex") assert_receive notification("window/logMessage", %{ "message" => "[ElixirLS Dialyzer] Found " <> _ @@ -164,10 +164,10 @@ defmodule ElixirLS.LanguageServer.DialyzerTest do @tag slow: true, fixture: true test "reports dialyxir_long formatted error", %{server: server} do in_fixture(__DIR__, "dialyzer", fn -> - file_a = SourceFile.path_to_uri(Path.absname("lib/a.ex")) + file_a = SourceFile.Path.to_uri(Path.absname("lib/a.ex")) capture_log(fn -> - root_uri = SourceFile.path_to_uri(File.cwd!()) + root_uri = SourceFile.Path.to_uri(File.cwd!()) Server.receive_packet(server, initialize_req(1, root_uri, %{})) Server.receive_packet( @@ -218,10 +218,10 @@ defmodule ElixirLS.LanguageServer.DialyzerTest do @tag slow: true, fixture: true test "reports dialyxir_short formatted error", %{server: server} do in_fixture(__DIR__, "dialyzer", fn -> - file_a = SourceFile.path_to_uri(Path.absname("lib/a.ex")) + file_a = SourceFile.Path.to_uri(Path.absname("lib/a.ex")) capture_log(fn -> - root_uri = SourceFile.path_to_uri(File.cwd!()) + root_uri = SourceFile.Path.to_uri(File.cwd!()) Server.receive_packet(server, initialize_req(1, root_uri, %{})) Server.receive_packet( @@ -263,10 +263,10 @@ defmodule ElixirLS.LanguageServer.DialyzerTest do @tag slow: true, fixture: true test "reports dialyzer_formatted error", %{server: server} do in_fixture(__DIR__, "dialyzer", fn -> - file_a = SourceFile.path_to_uri(Path.absname("lib/a.ex")) + file_a = SourceFile.Path.to_uri(Path.absname("lib/a.ex")) capture_log(fn -> - root_uri = SourceFile.path_to_uri(File.cwd!()) + root_uri = SourceFile.Path.to_uri(File.cwd!()) Server.receive_packet(server, initialize_req(1, root_uri, %{})) Server.receive_packet( @@ -309,10 +309,10 @@ defmodule ElixirLS.LanguageServer.DialyzerTest do @tag slow: true, fixture: true test "reports dialyxir_short error in umbrella", %{server: server} do in_fixture(__DIR__, "umbrella_dialyzer", fn -> - file_a = SourceFile.path_to_uri(Path.absname("apps/app1/lib/app1.ex")) + file_a = SourceFile.Path.to_uri(Path.absname("apps/app1/lib/app1.ex")) capture_log(fn -> - root_uri = SourceFile.path_to_uri(File.cwd!()) + root_uri = SourceFile.Path.to_uri(File.cwd!()) Server.receive_packet(server, initialize_req(1, root_uri, %{})) Server.receive_packet( @@ -353,10 +353,10 @@ defmodule ElixirLS.LanguageServer.DialyzerTest do test "clears diagnostics when source files are deleted", %{server: server} do in_fixture(__DIR__, "dialyzer", fn -> - file_a = SourceFile.path_to_uri(Path.absname("lib/a.ex")) + file_a = SourceFile.Path.to_uri(Path.absname("lib/a.ex")) capture_log(fn -> - root_uri = SourceFile.path_to_uri(File.cwd!()) + root_uri = SourceFile.Path.to_uri(File.cwd!()) Server.receive_packet(server, initialize_req(1, root_uri, %{})) Server.receive_packet( @@ -380,8 +380,8 @@ defmodule ElixirLS.LanguageServer.DialyzerTest do @tag slow: true, fixture: true test "protocol rebuild does not trigger consolidation warnings", %{server: server} do in_fixture(__DIR__, "protocols", fn -> - root_uri = SourceFile.path_to_uri(File.cwd!()) - uri = SourceFile.path_to_uri(Path.absname("lib/implementations.ex")) + root_uri = SourceFile.Path.to_uri(File.cwd!()) + uri = SourceFile.Path.to_uri(Path.absname("lib/implementations.ex")) Server.receive_packet(server, initialize_req(1, root_uri, %{})) Server.receive_packet(server, notification("initialized")) @@ -454,10 +454,10 @@ defmodule ElixirLS.LanguageServer.DialyzerTest do @tag slow: true, fixture: true test "do not suggests contracts if not enabled", %{server: server} do in_fixture(__DIR__, "dialyzer", fn -> - file_c = SourceFile.path_to_uri(Path.absname("lib/c.ex")) + file_c = SourceFile.Path.to_uri(Path.absname("lib/c.ex")) capture_log(fn -> - root_uri = SourceFile.path_to_uri(File.cwd!()) + root_uri = SourceFile.Path.to_uri(File.cwd!()) Server.receive_packet(server, initialize_req(1, root_uri, %{})) Server.receive_packet( @@ -491,10 +491,10 @@ defmodule ElixirLS.LanguageServer.DialyzerTest do @tag slow: true, fixture: true test "suggests contracts if enabled and applies suggestion", %{server: server} do in_fixture(__DIR__, "dialyzer", fn -> - file_c = SourceFile.path_to_uri(Path.absname("lib/c.ex")) + file_c = SourceFile.Path.to_uri(Path.absname("lib/c.ex")) capture_log(fn -> - root_uri = SourceFile.path_to_uri(File.cwd!()) + root_uri = SourceFile.Path.to_uri(File.cwd!()) Server.receive_packet(server, initialize_req(1, root_uri, %{})) Server.receive_packet( diff --git a/apps/language_server/test/providers/definition_test.exs b/apps/language_server/test/providers/definition_test.exs index 16ef598f8..9b0539e96 100644 --- a/apps/language_server/test/providers/definition_test.exs +++ b/apps/language_server/test/providers/definition_test.exs @@ -10,10 +10,10 @@ defmodule ElixirLS.LanguageServer.Providers.DefinitionTest do test "find definition remote function call" do file_path = FixtureHelpers.get_path("references_remote.ex") text = File.read!(file_path) - uri = SourceFile.path_to_uri(file_path) + uri = SourceFile.Path.to_uri(file_path) b_file_path = FixtureHelpers.get_path("references_referenced.ex") - b_uri = SourceFile.path_to_uri(b_file_path) + b_uri = SourceFile.Path.to_uri(b_file_path) {line, char} = {4, 28} @@ -34,10 +34,10 @@ defmodule ElixirLS.LanguageServer.Providers.DefinitionTest do test "find definition remote macro call" do file_path = FixtureHelpers.get_path("references_remote.ex") text = File.read!(file_path) - uri = SourceFile.path_to_uri(file_path) + uri = SourceFile.Path.to_uri(file_path) b_file_path = FixtureHelpers.get_path("references_referenced.ex") - b_uri = SourceFile.path_to_uri(b_file_path) + b_uri = SourceFile.Path.to_uri(b_file_path) {line, char} = {8, 28} @@ -58,10 +58,10 @@ defmodule ElixirLS.LanguageServer.Providers.DefinitionTest do test "find definition imported function call" do file_path = FixtureHelpers.get_path("references_imported.ex") text = File.read!(file_path) - uri = SourceFile.path_to_uri(file_path) + uri = SourceFile.Path.to_uri(file_path) b_file_path = FixtureHelpers.get_path("references_referenced.ex") - b_uri = SourceFile.path_to_uri(b_file_path) + b_uri = SourceFile.Path.to_uri(b_file_path) {line, char} = {4, 5} @@ -82,10 +82,10 @@ defmodule ElixirLS.LanguageServer.Providers.DefinitionTest do test "find definition imported macro call" do file_path = FixtureHelpers.get_path("references_imported.ex") text = File.read!(file_path) - uri = SourceFile.path_to_uri(file_path) + uri = SourceFile.Path.to_uri(file_path) b_file_path = FixtureHelpers.get_path("references_referenced.ex") - b_uri = SourceFile.path_to_uri(b_file_path) + b_uri = SourceFile.Path.to_uri(b_file_path) {line, char} = {8, 5} @@ -106,10 +106,10 @@ defmodule ElixirLS.LanguageServer.Providers.DefinitionTest do test "find definition local function call" do file_path = FixtureHelpers.get_path("references_referenced.ex") text = File.read!(file_path) - uri = SourceFile.path_to_uri(file_path) + uri = SourceFile.Path.to_uri(file_path) b_file_path = FixtureHelpers.get_path("references_referenced.ex") - b_uri = SourceFile.path_to_uri(b_file_path) + b_uri = SourceFile.Path.to_uri(b_file_path) {line, char} = {15, 5} @@ -130,10 +130,10 @@ defmodule ElixirLS.LanguageServer.Providers.DefinitionTest do test "find definition local macro call" do file_path = FixtureHelpers.get_path("references_referenced.ex") text = File.read!(file_path) - uri = SourceFile.path_to_uri(file_path) + uri = SourceFile.Path.to_uri(file_path) b_file_path = FixtureHelpers.get_path("references_referenced.ex") - b_uri = SourceFile.path_to_uri(b_file_path) + b_uri = SourceFile.Path.to_uri(b_file_path) {line, char} = {19, 5} @@ -154,10 +154,10 @@ defmodule ElixirLS.LanguageServer.Providers.DefinitionTest do test "find definition variable" do file_path = FixtureHelpers.get_path("references_referenced.ex") text = File.read!(file_path) - uri = SourceFile.path_to_uri(file_path) + uri = SourceFile.Path.to_uri(file_path) b_file_path = FixtureHelpers.get_path("references_referenced.ex") - b_uri = SourceFile.path_to_uri(b_file_path) + b_uri = SourceFile.Path.to_uri(b_file_path) {line, char} = {4, 13} @@ -178,10 +178,10 @@ defmodule ElixirLS.LanguageServer.Providers.DefinitionTest do test "find definition attribute" do file_path = FixtureHelpers.get_path("references_referenced.ex") text = File.read!(file_path) - uri = SourceFile.path_to_uri(file_path) + uri = SourceFile.Path.to_uri(file_path) b_file_path = FixtureHelpers.get_path("references_referenced.ex") - b_uri = SourceFile.path_to_uri(b_file_path) + b_uri = SourceFile.Path.to_uri(b_file_path) {line, char} = {27, 5} diff --git a/apps/language_server/test/providers/execute_command/mix_clean_test.exs b/apps/language_server/test/providers/execute_command/mix_clean_test.exs index a82cfe529..e1c325944 100644 --- a/apps/language_server/test/providers/execute_command/mix_clean_test.exs +++ b/apps/language_server/test/providers/execute_command/mix_clean_test.exs @@ -13,7 +13,7 @@ defmodule ElixirLS.LanguageServer.Providers.ExecuteCommand.MixCleanTest do @tag fixture: true test "mix clean", %{server: server} do in_fixture(Path.join(__DIR__, "../.."), "clean", fn -> - root_uri = SourceFile.path_to_uri(File.cwd!()) + root_uri = SourceFile.Path.to_uri(File.cwd!()) Server.receive_packet(server, initialize_req(1, root_uri, %{})) Server.receive_packet( diff --git a/apps/language_server/test/providers/formatting_test.exs b/apps/language_server/test/providers/formatting_test.exs index 30b833c53..7fd4c7664 100644 --- a/apps/language_server/test/providers/formatting_test.exs +++ b/apps/language_server/test/providers/formatting_test.exs @@ -10,7 +10,7 @@ defmodule ElixirLS.LanguageServer.Providers.FormattingTest do test "no mixfile" do in_fixture(Path.join(__DIR__, ".."), "no_mixfile", fn -> path = "lib/file.ex" - uri = SourceFile.path_to_uri(path) + uri = SourceFile.Path.to_uri(path) text = """ defmodule MyModule do @@ -60,7 +60,7 @@ defmodule ElixirLS.LanguageServer.Providers.FormattingTest do test "no project dir" do in_fixture(Path.join(__DIR__, ".."), "no_mixfile", fn -> path = "lib/file.ex" - uri = SourceFile.path_to_uri(path) + uri = SourceFile.Path.to_uri(path) text = """ defmodule MyModule do @@ -110,7 +110,7 @@ defmodule ElixirLS.LanguageServer.Providers.FormattingTest do test "Formats a file with LF line endings" do in_fixture(Path.join(__DIR__, ".."), "formatter", fn -> path = "lib/file.ex" - uri = SourceFile.path_to_uri(path) + uri = SourceFile.Path.to_uri(path) text = """ defmodule MyModule do @@ -160,7 +160,7 @@ defmodule ElixirLS.LanguageServer.Providers.FormattingTest do test "Formats a file with CRLF line endings" do in_fixture(Path.join(__DIR__, ".."), "formatter", fn -> path = "lib/file.ex" - uri = SourceFile.path_to_uri(path) + uri = SourceFile.Path.to_uri(path) text = """ defmodule MyModule do @@ -247,7 +247,7 @@ defmodule ElixirLS.LanguageServer.Providers.FormattingTest do test "elixir formatter does not support CR line endings" do in_fixture(Path.join(__DIR__, ".."), "formatter", fn -> path = "lib/file.ex" - uri = SourceFile.path_to_uri(path) + uri = SourceFile.Path.to_uri(path) text = """ defmodule MyModule do @@ -278,7 +278,7 @@ defmodule ElixirLS.LanguageServer.Providers.FormattingTest do test "formatting preserves line indings inside a string" do in_fixture(Path.join(__DIR__, ".."), "formatter", fn -> path = "lib/file.ex" - uri = SourceFile.path_to_uri(path) + uri = SourceFile.Path.to_uri(path) text = """ defmodule MyModule do @@ -333,7 +333,7 @@ defmodule ElixirLS.LanguageServer.Providers.FormattingTest do test "returns an error when formatting a file with a syntax error" do in_fixture(Path.join(__DIR__, ".."), "formatter", fn -> path = "lib/file.ex" - uri = SourceFile.path_to_uri(path) + uri = SourceFile.Path.to_uri(path) text = """ defmodule MyModule do @@ -362,7 +362,7 @@ defmodule ElixirLS.LanguageServer.Providers.FormattingTest do test "Proper utf-16 format: emoji 😀" do in_fixture(Path.join(__DIR__, ".."), "formatter", fn -> path = "lib/file.ex" - uri = SourceFile.path_to_uri(path) + uri = SourceFile.Path.to_uri(path) text = """ IO.puts "😀" @@ -401,7 +401,7 @@ defmodule ElixirLS.LanguageServer.Providers.FormattingTest do test "Proper utf-16 format: emoji 🏳️‍🌈" do in_fixture(Path.join(__DIR__, ".."), "formatter", fn -> path = "lib/file.ex" - uri = SourceFile.path_to_uri(path) + uri = SourceFile.Path.to_uri(path) text = """ IO.puts "🏳️‍🌈" @@ -440,7 +440,7 @@ defmodule ElixirLS.LanguageServer.Providers.FormattingTest do test "Proper utf-16 format: zalgo" do in_fixture(Path.join(__DIR__, ".."), "formatter", fn -> path = "lib/file.ex" - uri = SourceFile.path_to_uri(path) + uri = SourceFile.Path.to_uri(path) text = """ IO.puts "ẕ̸͇̞̲͇͕̹̙̄͆̇͂̏̊͒̒̈́́̕͘͠͝à̵̢̛̟̞͚̟͖̻̹̮̘͚̻͍̇͂̂̅́̎̉͗́́̃̒l̴̻̳͉̖̗͖̰̠̗̃̈́̓̓̍̅͝͝͝g̷̢͚̠̜̿̊́̋͗̔ȍ̶̹̙̅̽̌̒͌͋̓̈́͑̏͑͊͛͘ ̸̨͙̦̫̪͓̠̺̫̖͙̫̏͂̒̽́̿̂̊́͂͋͜͠͝͝ṭ̴̜͎̮͉̙͍͔̜̾͋͒̓̏̉̄͘͠͝ͅę̷̡̭̹̰̺̩̠͓͌̃̕͜͝ͅͅx̵̧͍̦͈͍̝͖͙̘͎̥͕̾̾̍̀̿̔̄̑̈͝t̸̛͇̀̕" @@ -522,6 +522,6 @@ defmodule ElixirLS.LanguageServer.Providers.FormattingTest do } File.write!(path, " asd = 1") - Formatting.format(source_file, SourceFile.path_to_uri(path), project_dir) + Formatting.format(source_file, SourceFile.Path.to_uri(path), project_dir) end end diff --git a/apps/language_server/test/providers/implementation_test.exs b/apps/language_server/test/providers/implementation_test.exs index 3f63e0d00..f95f0eca4 100644 --- a/apps/language_server/test/providers/implementation_test.exs +++ b/apps/language_server/test/providers/implementation_test.exs @@ -14,7 +14,7 @@ defmodule ElixirLS.LanguageServer.Providers.ImplementationTest do file_path = FixtureHelpers.get_path("example_behaviour.ex") text = File.read!(file_path) - uri = SourceFile.path_to_uri(file_path) + uri = SourceFile.Path.to_uri(file_path) {line, char} = {0, 43} diff --git a/apps/language_server/test/providers/references_test.exs b/apps/language_server/test/providers/references_test.exs index 3882f4bc9..f26d87cc1 100644 --- a/apps/language_server/test/providers/references_test.exs +++ b/apps/language_server/test/providers/references_test.exs @@ -38,7 +38,7 @@ defmodule ElixirLS.LanguageServer.Providers.ReferencesTest do test "finds local, remote and imported references to a function" do file_path = FixtureHelpers.get_path("references_referenced.ex") text = File.read!(file_path) - uri = SourceFile.path_to_uri(file_path) + uri = SourceFile.Path.to_uri(file_path) {line, char} = {1, 8} @@ -58,7 +58,7 @@ defmodule ElixirLS.LanguageServer.Providers.ReferencesTest do test "finds local, remote and imported references to a macro" do file_path = FixtureHelpers.get_path("references_referenced.ex") text = File.read!(file_path) - uri = SourceFile.path_to_uri(file_path) + uri = SourceFile.Path.to_uri(file_path) {line, char} = {8, 12} @@ -78,7 +78,7 @@ defmodule ElixirLS.LanguageServer.Providers.ReferencesTest do test "find a references to a macro generated function call" do file_path = FixtureHelpers.get_path("uses_macro_a.ex") text = File.read!(file_path) - uri = SourceFile.path_to_uri(file_path) + uri = SourceFile.Path.to_uri(file_path) {line, char} = {6, 13} ElixirLS.Test.TextLoc.annotate_assert(file_path, line, char, """ @@ -100,7 +100,7 @@ defmodule ElixirLS.LanguageServer.Providers.ReferencesTest do test "finds a references to a macro imported function call" do file_path = FixtureHelpers.get_path("uses_macro_a.ex") text = File.read!(file_path) - uri = SourceFile.path_to_uri(file_path) + uri = SourceFile.Path.to_uri(file_path) {line, char} = {10, 4} ElixirLS.Test.TextLoc.annotate_assert(file_path, line, char, """ @@ -122,7 +122,7 @@ defmodule ElixirLS.LanguageServer.Providers.ReferencesTest do test "finds references to a variable" do file_path = FixtureHelpers.get_path("references_referenced.ex") text = File.read!(file_path) - uri = SourceFile.path_to_uri(file_path) + uri = SourceFile.Path.to_uri(file_path) {line, char} = {4, 14} ElixirLS.Test.TextLoc.annotate_assert(file_path, line, char, """ @@ -151,7 +151,7 @@ defmodule ElixirLS.LanguageServer.Providers.ReferencesTest do test "finds references to an attribute" do file_path = FixtureHelpers.get_path("references_referenced.ex") text = File.read!(file_path) - uri = SourceFile.path_to_uri(file_path) + uri = SourceFile.Path.to_uri(file_path) {line, char} = {24, 5} ElixirLS.Test.TextLoc.annotate_assert(file_path, line, char, """ diff --git a/apps/language_server/test/providers/workspace_symbols_test.exs b/apps/language_server/test/providers/workspace_symbols_test.exs index dcf86a3af..5699ba0c1 100644 --- a/apps/language_server/test/providers/workspace_symbols_test.exs +++ b/apps/language_server/test/providers/workspace_symbols_test.exs @@ -14,7 +14,7 @@ defmodule ElixirLS.LanguageServer.Providers.WorkspaceSymbolsTest do fixture_uri = ElixirLS.LanguageServer.Fixtures.WorkspaceSymbols.module_info(:compile)[:source] |> List.to_string() - |> ElixirLS.LanguageServer.SourceFile.path_to_uri() + |> ElixirLS.LanguageServer.SourceFile.Path.to_uri() :sys.replace_state(pid, fn _ -> %{ diff --git a/apps/language_server/test/server_test.exs b/apps/language_server/test/server_test.exs index 9d40135a6..2f78065f5 100644 --- a/apps/language_server/test/server_test.exs +++ b/apps/language_server/test/server_test.exs @@ -24,7 +24,7 @@ defmodule ElixirLS.LanguageServer.ServerTest do end defp root_uri do - SourceFile.path_to_uri(File.cwd!()) + SourceFile.Path.to_uri(File.cwd!()) end describe "initialize" do @@ -532,7 +532,7 @@ defmodule ElixirLS.LanguageServer.ServerTest do ) in_fixture(__DIR__, "references", fn -> - uri = SourceFile.path_to_uri("lib/a.ex") + uri = SourceFile.Path.to_uri("lib/a.ex") fake_initialize(server) Server.receive_packet(server, did_open(uri, "elixir", 1, code)) Server.receive_packet(server, did_change(uri, 1, content_changes)) @@ -567,7 +567,7 @@ defmodule ElixirLS.LanguageServer.ServerTest do ) in_fixture(__DIR__, "references", fn -> - uri = SourceFile.path_to_uri("lib/a.ex") + uri = SourceFile.Path.to_uri("lib/a.ex") fake_initialize(server) Server.receive_packet(server, did_open(uri, "elixir", 1, code)) Server.receive_packet(server, did_change(uri, 1, content_changes)) @@ -751,7 +751,7 @@ defmodule ElixirLS.LanguageServer.ServerTest do uri = ElixirLS.LanguageServer.Fixtures.ExampleBehaviour.module_info()[:compile][:source] |> to_string - |> SourceFile.path_to_uri() + |> SourceFile.Path.to_uri() assert_receive( response(1, %{ @@ -784,7 +784,7 @@ defmodule ElixirLS.LanguageServer.ServerTest do test "implementations found", %{server: server} do file_path = FixtureHelpers.get_path("example_behaviour.ex") text = File.read!(file_path) - uri = SourceFile.path_to_uri(file_path) + uri = SourceFile.Path.to_uri(file_path) fake_initialize(server) Server.receive_packet(server, did_open(uri, "elixir", 1, text)) @@ -919,7 +919,7 @@ defmodule ElixirLS.LanguageServer.ServerTest do uri = Path.join([root_uri(), "file.ex"]) uri - |> SourceFile.abs_path_from_uri() + |> SourceFile.Path.absolute_from_uri() |> File.write!("") code = """ @@ -1027,7 +1027,7 @@ defmodule ElixirLS.LanguageServer.ServerTest do @tag :fixture test "reports build diagnostics", %{server: server} do in_fixture(__DIR__, "build_errors", fn -> - error_file = SourceFile.path_to_uri("lib/has_error.ex") + error_file = SourceFile.Path.to_uri("lib/has_error.ex") initialize(server) @@ -1050,7 +1050,7 @@ defmodule ElixirLS.LanguageServer.ServerTest do @tag :fixture test "reports token missing error diagnostics", %{server: server} do in_fixture(__DIR__, "token_missing_error", fn -> - error_file = SourceFile.path_to_uri("lib/has_error.ex") + error_file = SourceFile.Path.to_uri("lib/has_error.ex") initialize(server) @@ -1073,7 +1073,7 @@ defmodule ElixirLS.LanguageServer.ServerTest do @tag :fixture test "reports build diagnostics on external resources", %{server: server} do in_fixture(__DIR__, "build_errors_on_external_resource", fn -> - error_file = SourceFile.path_to_uri("lib/template.eex") + error_file = SourceFile.Path.to_uri("lib/template.eex") initialize(server) @@ -1097,9 +1097,9 @@ defmodule ElixirLS.LanguageServer.ServerTest do test "finds references in non-umbrella project", %{server: server} do in_fixture(__DIR__, "references", fn -> file_path = "lib/b.ex" - file_uri = SourceFile.path_to_uri(file_path) + file_uri = SourceFile.Path.to_uri(file_path) text = File.read!(file_path) - reference_uri = SourceFile.path_to_uri("lib/a.ex") + reference_uri = SourceFile.Path.to_uri("lib/a.ex") Build.set_compiler_options() @@ -1131,9 +1131,9 @@ defmodule ElixirLS.LanguageServer.ServerTest do test "finds references in umbrella project", %{server: server} do in_fixture(__DIR__, "umbrella", fn -> file_path = "apps/app2/lib/app2.ex" - file_uri = SourceFile.path_to_uri(file_path) + file_uri = SourceFile.Path.to_uri(file_path) text = File.read!(file_path) - reference_uri = SourceFile.path_to_uri("apps/app1/lib/app1.ex") + reference_uri = SourceFile.Path.to_uri("apps/app1/lib/app1.ex") Build.set_compiler_options() @@ -1182,7 +1182,7 @@ defmodule ElixirLS.LanguageServer.ServerTest do wait_until_compiled(server) file_path = "apps/app1/lib/bar.ex" - uri = SourceFile.path_to_uri(file_path) + uri = SourceFile.Path.to_uri(file_path) code = """ defmodule Bar do @@ -1216,15 +1216,13 @@ defmodule ElixirLS.LanguageServer.ServerTest do test "returns code lenses for runnable tests", %{server: server} do in_fixture(__DIR__, "test_code_lens", fn -> file_path = "test/fixture_test.exs" - file_uri = SourceFile.path_to_uri(file_path) + file_uri = SourceFile.Path.to_uri(file_path) # this is not an abs path as returned by Path.absname # on Windows it's c:\asdf instead of c:/asdf - file_absolute_path = SourceFile.path_from_uri(file_uri) + file_absolute_path = SourceFile.Path.from_uri(file_uri) text = File.read!(file_path) - project_dir = - root_uri() - |> SourceFile.abs_path_from_uri() + project_dir = SourceFile.Path.absolute_from_uri(root_uri()) initialize(server) @@ -1289,7 +1287,7 @@ defmodule ElixirLS.LanguageServer.ServerTest do } do in_fixture(__DIR__, "test_code_lens", fn -> file_path = "test/fixture_test.exs" - file_uri = SourceFile.path_to_uri(file_path) + file_uri = SourceFile.Path.to_uri(file_path) text = File.read!(file_path) fake_initialize(server) @@ -1313,10 +1311,10 @@ defmodule ElixirLS.LanguageServer.ServerTest do } do in_fixture(__DIR__, "test_code_lens_custom_paths_and_pattern", fn -> file_path = "custom_path/fixture_custom_test.exs" - file_uri = SourceFile.path_to_uri(file_path) - file_absolute_path = SourceFile.path_from_uri(file_uri) + file_uri = SourceFile.Path.to_uri(file_path) + file_absolute_path = SourceFile.Path.from_uri(file_uri) text = File.read!(file_path) - project_dir = SourceFile.path_from_uri(root_uri()) + project_dir = SourceFile.Path.from_uri(root_uri()) initialize(server) @@ -1382,10 +1380,10 @@ defmodule ElixirLS.LanguageServer.ServerTest do } do in_fixture(__DIR__, "umbrella_test_code_lens_custom_path_and_pattern", fn -> file_path = "apps/app1/custom_path/fixture_custom_test.exs" - file_uri = SourceFile.path_to_uri(file_path) - file_absolute_path = SourceFile.path_from_uri(file_uri) + file_uri = SourceFile.Path.to_uri(file_path) + file_absolute_path = SourceFile.Path.from_uri(file_uri) text = File.read!(file_path) - project_dir = SourceFile.path_from_uri("#{root_uri()}/apps/app1") + project_dir = SourceFile.Path.from_uri("#{root_uri()}/apps/app1") initialize(server) diff --git a/apps/language_server/test/source_file/path_test.exs b/apps/language_server/test/source_file/path_test.exs new file mode 100644 index 000000000..837013d42 --- /dev/null +++ b/apps/language_server/test/source_file/path_test.exs @@ -0,0 +1,191 @@ +defmodule ElixirLS.LanguageServer.SourceFile.PathTest do + use ExUnit.Case + use Patch + + import ElixirLS.LanguageServer.SourceFile.Path + import ElixirLS.LanguageServer.Test.PlatformTestHelpers + + defp patch_os(os_type, fun) do + test = self() + + spawn(fn -> + patch(ElixirLS.LanguageServer.SourceFile.Path, :os_type, os_type) + + try do + rv = fun.() + send(test, {:return, rv}) + rescue + e -> + send(test, {:raise, e, __STACKTRACE__}) + end + end) + + receive do + {:return, rv} -> + rv + + {:raise, %ExUnit.AssertionError{} = e, stack} -> + new_message = "In O/S #{inspect(os_type)} #{e.message}" + reraise(%ExUnit.AssertionError{e | message: new_message}, stack) + + {:raise, error, stack} -> + reraise(error, stack) + end + end + + def with_os(:windows, fun) do + patch_os({:win32, :whatever}, fun) + end + + def with_os(:linux, fun) do + patch_os({:unix, :linux}, fun) + end + + def with_os(:macos, fun) do + patch_os({:unix, :darwin}, fun) + end + + describe "from_uri/1" do + # tests based on cases from https://github.com/microsoft/vscode-uri/blob/master/src/test/uri.test.ts + + test "unix" do + with_os(:windows, fn -> + assert from_uri("file:///some/path") == "\\some\\path" + assert from_uri("file:///some/path/") == "\\some\\path\\" + assert from_uri("file:///nodes%2B%23.ex") == "\\nodes+#.ex" + end) + + with_os(:linux, fn -> + assert from_uri("file:///some/path") == "/some/path" + assert from_uri("file:///some/path/") == "/some/path/" + assert from_uri("file:///nodes%2B%23.ex") == "/nodes+#.ex" + end) + end + + test "UNC" do + with_os(:windows, fn -> + assert from_uri("file://shares/files/c%23/p.cs") == "\\\\shares\\files\\c#\\p.cs" + + assert from_uri("file://monacotools1/certificates/SSL/") == + "\\\\monacotools1\\certificates\\SSL\\" + + assert from_uri("file://monacotools1/") == "\\\\monacotools1\\" + end) + + with_os(:linux, fn -> + assert from_uri("file://shares/files/c%23/p.cs") == "//shares/files/c#/p.cs" + + assert from_uri("file://monacotools1/certificates/SSL/") == + "//monacotools1/certificates/SSL/" + + assert from_uri("file://monacotools1/") == "//monacotools1/" + end) + end + + test "no `path` in URI" do + with_os(:windows, fn -> + assert from_uri("file://%2Fhome%2Fticino%2Fdesktop%2Fcpluscplus%2Ftest.cpp") == "\\" + end) + + with_os(:linux, fn -> + assert from_uri("file://%2Fhome%2Fticino%2Fdesktop%2Fcpluscplus%2Ftest.cpp") == "/" + end) + end + + test "windows drive letter" do + with_os(:windows, fn -> + assert from_uri("file:///c:/test/me") == "c:\\test\\me" + assert from_uri("file:///c%3A/test/me") == "c:\\test\\me" + assert from_uri("file:///C:/test/me/") == "c:\\test\\me\\" + assert from_uri("file:///_:/path") == "\\_:\\path" + + assert from_uri( + "file:///c:/Source/Z%C3%BCrich%20or%20Zurich%20(%CB%88zj%CA%8A%C9%99r%C9%AAk,/Code/resources/app/plugins" + ) == "c:\\Source\\Zürich or Zurich (ˈzjʊərɪk,\\Code\\resources\\app\\plugins" + end) + + with_os(:linux, fn -> + assert from_uri("file:///c:/test/me") == "/c:/test/me" + assert from_uri("file:///c%3A/test/me") == "/c:/test/me" + assert from_uri("file:///C:/test/me/") == "/C:/test/me/" + assert from_uri("file:///_:/path") == "/_:/path" + + assert from_uri( + "file:///c:/Source/Z%C3%BCrich%20or%20Zurich%20(%CB%88zj%CA%8A%C9%99r%C9%AAk,/Code/resources/app/plugins" + ) == "/c:/Source/Zürich or Zurich (ˈzjʊərɪk,/Code/resources/app/plugins" + end) + end + + test "wrong schema" do + assert_raise ArgumentError, fn -> + from_uri("untitled:Untitled-1") + end + + assert_raise ArgumentError, fn -> + from_uri("unsaved://343C3EE7-D575-486D-9D33-93AFFAF773BD") + end + end + end + + describe "to_uri/1" do + # tests based on cases from https://github.com/microsoft/vscode-uri/blob/master/src/test/uri.test.ts + test "unix path" do + with_os(:linux, fn -> + assert "file:///nodes%2B%23.ex" == to_uri("/nodes+#.ex") + assert "file:///coding/c%23/project1" == to_uri("/coding/c#/project1") + + assert "file:///Users/jrieken/Code/_samples/18500/M%C3%B6del%20%2B%20Other%20Th%C3%AEng%C3%9F/model.js" == + to_uri("/Users/jrieken/Code/_samples/18500/Mödel + Other Thîngß/model.js") + + assert "file:///foo/%25A0.txt" == to_uri("/foo/%A0.txt") + assert "file:///foo/%252e.txt" == to_uri("/foo/%2e.txt") + end) + end + + test "windows path" do + if is_windows() do + assert "file:///c%3A/win/path" == to_uri("c:/win/path") + assert "file:///c%3A/win/path" == to_uri("C:/win/path") + assert "file:///c%3A/win/path" == to_uri("c:/win/path/") + assert "file:///c%3A/win/path" == to_uri("/c:/win/path") + + assert "file:///c%3A/win/path" == to_uri("c:\\win\\path") + assert "file:///c%3A/win/path" == to_uri("c:\\win/path") + + assert "file:///c%3A/test%20with%20%25/path" == + to_uri("c:\\test with %\\path") + + assert "file:///c%3A/test%20with%20%2525/c%23code" == + to_uri("c:\\test with %25\\c#code") + end + end + + test "relative path" do + cwd = File.cwd!() + + uri = to_uri("a.file") + + assert from_uri(uri) == + cwd + |> Path.join("a.file") + |> maybe_convert_path_separators() + + uri = to_uri("./foo/bar") + + assert from_uri(uri) == + cwd + |> Path.join("foo/bar") + |> maybe_convert_path_separators + end + + test "UNC path" do + if is_windows() do + assert "file://sh%C3%A4res/path/c%23/plugin.json" == + to_uri("\\\\shäres\\path\\c#\\plugin.json") + + assert "file://localhost/c%24/GitDevelopment/express" == + to_uri("\\\\localhost\\c$\\GitDevelopment\\express") + end + end + end +end diff --git a/apps/language_server/test/source_file_test.exs b/apps/language_server/test/source_file_test.exs index 86e69d00f..1909e66aa 100644 --- a/apps/language_server/test/source_file_test.exs +++ b/apps/language_server/test/source_file_test.exs @@ -643,186 +643,6 @@ defmodule ElixirLS.LanguageServer.SourceFileTest do end end - # tests basing on cases from https://github.com/microsoft/vscode-uri/blob/master/src/test/uri.test.ts - describe "path_from_uri" do - test "unix" do - path = SourceFile.path_from_uri("file:///some/path") - - if is_windows() do - assert path == "\\some\\path" - else - assert path == "/some/path" - end - - path = SourceFile.path_from_uri("file:///some/path/") - - if is_windows() do - assert path == "\\some\\path\\" - else - assert path == "/some/path/" - end - - path = SourceFile.path_from_uri("file:///nodes%2B%23.ex") - - if is_windows() do - assert path == "\\nodes+#.ex" - else - assert path == "/nodes+#.ex" - end - end - - test "UNC" do - path = SourceFile.path_from_uri("file://shares/files/c%23/p.cs") - - if is_windows() do - assert path == "\\\\shares\\files\\c#\\p.cs" - else - assert path == "//shares/files/c#/p.cs" - end - - path = SourceFile.path_from_uri("file://monacotools1/certificates/SSL/") - - if is_windows() do - assert path == "\\\\monacotools1\\certificates\\SSL\\" - else - assert path == "//monacotools1/certificates/SSL/" - end - - path = SourceFile.path_from_uri("file://monacotools1/") - - if is_windows() do - assert path == "\\\\monacotools1\\" - else - assert path == "//monacotools1/" - end - end - - test "no `path` in URI" do - path = SourceFile.path_from_uri("file://%2Fhome%2Fticino%2Fdesktop%2Fcpluscplus%2Ftest.cpp") - - if is_windows() do - assert path == "\\" - else - assert path == "/" - end - end - - test "windows drive letter" do - path = SourceFile.path_from_uri("file:///c:/test/me") - - if is_windows() do - assert path == "c:\\test\\me" - else - assert path == "/c:/test/me" - end - - path = SourceFile.path_from_uri("file:///c%3A/test/me") - - if is_windows() do - assert path == "c:\\test\\me" - else - assert path == "/c:/test/me" - end - - path = SourceFile.path_from_uri("file:///C:/test/me/") - - if is_windows() do - assert path == "c:\\test\\me\\" - else - assert path == "/C:/test/me/" - end - - path = SourceFile.path_from_uri("file:///_:/path") - - if is_windows() do - assert path == "\\_:\\path" - else - assert path == "/_:/path" - end - - path = - SourceFile.path_from_uri( - "file:///c:/Source/Z%C3%BCrich%20or%20Zurich%20(%CB%88zj%CA%8A%C9%99r%C9%AAk,/Code/resources/app/plugins" - ) - - if is_windows() do - assert path == "c:\\Source\\Zürich or Zurich (ˈzjʊərɪk,\\Code\\resources\\app\\plugins" - else - assert path == "/c:/Source/Zürich or Zurich (ˈzjʊərɪk,/Code/resources/app/plugins" - end - end - - test "wrong schema" do - assert_raise ArgumentError, fn -> - SourceFile.path_from_uri("untitled:Untitled-1") - end - - assert_raise ArgumentError, fn -> - SourceFile.path_from_uri("unsaved://343C3EE7-D575-486D-9D33-93AFFAF773BD") - end - end - end - - # tests basing on cases from https://github.com/microsoft/vscode-uri/blob/master/src/test/uri.test.ts - describe "path_to_uri" do - test "unix path" do - unless is_windows() do - assert "file:///nodes%2B%23.ex" == SourceFile.path_to_uri("/nodes+#.ex") - assert "file:///coding/c%23/project1" == SourceFile.path_to_uri("/coding/c#/project1") - - assert "file:///Users/jrieken/Code/_samples/18500/M%C3%B6del%20%2B%20Other%20Th%C3%AEng%C3%9F/model.js" == - SourceFile.path_to_uri( - "/Users/jrieken/Code/_samples/18500/Mödel + Other Thîngß/model.js" - ) - - assert "file:///foo/%25A0.txt" == SourceFile.path_to_uri("/foo/%A0.txt") - assert "file:///foo/%252e.txt" == SourceFile.path_to_uri("/foo/%2e.txt") - end - end - - test "windows path" do - if is_windows() do - assert "file:///c%3A/win/path" == SourceFile.path_to_uri("c:/win/path") - assert "file:///c%3A/win/path" == SourceFile.path_to_uri("C:/win/path") - assert "file:///c%3A/win/path" == SourceFile.path_to_uri("c:/win/path/") - assert "file:///c%3A/win/path" == SourceFile.path_to_uri("/c:/win/path") - - assert "file:///c%3A/win/path" == SourceFile.path_to_uri("c:\\win\\path") - assert "file:///c%3A/win/path" == SourceFile.path_to_uri("c:\\win/path") - - assert "file:///c%3A/test%20with%20%25/path" == - SourceFile.path_to_uri("c:\\test with %\\path") - - assert "file:///c%3A/test%20with%20%2525/c%23code" == - SourceFile.path_to_uri("c:\\test with %25\\c#code") - end - end - - test "relative path" do - cwd = File.cwd!() - - uri = SourceFile.path_to_uri("a.file") - - assert SourceFile.path_from_uri(uri) == - maybe_convert_path_separators(Path.join(cwd, "a.file")) - - uri = SourceFile.path_to_uri("./foo/bar") - - assert SourceFile.path_from_uri(uri) == - maybe_convert_path_separators(Path.join(cwd, "foo/bar")) - end - - test "UNC path" do - if is_windows() do - assert "file://sh%C3%A4res/path/c%23/plugin.json" == - SourceFile.path_to_uri("\\\\shäres\\path\\c#\\plugin.json") - - assert "file://localhost/c%24/GitDevelopment/express" == - SourceFile.path_to_uri("\\\\localhost\\c$\\GitDevelopment\\express") - end - end - end - describe "positions" do test "lsp_position_to_elixir empty" do assert {1, 1} == SourceFile.lsp_position_to_elixir("", {0, 0}) From 031bd0a7246dc7f6d6e1077a975b55b2110f209c Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sat, 8 Oct 2022 00:37:37 +0200 Subject: [PATCH 071/125] Properly clean mix internal state so dep loading does not fail Fixes https://github.com/elixir-lsp/elixir-ls/issues/120 --- .../lib/language_server/build.ex | 29 ++++++++++--------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/apps/language_server/lib/language_server/build.ex b/apps/language_server/lib/language_server/build.ex index 6365cd976..bd7dad542 100644 --- a/apps/language_server/lib/language_server/build.ex +++ b/apps/language_server/lib/language_server/build.ex @@ -62,28 +62,31 @@ defmodule ElixirLS.LanguageServer.Build do mixfile = Path.absname(MixfileHelpers.mix_exs()) if File.exists?(mixfile) do - # FIXME: Private API - case Mix.ProjectStack.peek() do - %{file: ^mixfile, name: module} -> - # FIXME: Private API - Mix.Project.pop() - purge_module(module) - - _ -> - :ok + if module = Mix.Project.get() do + # FIXME: Private API + Mix.Project.pop() + purge_module(module) end - Mix.Task.clear() - - # Override build directory to avoid interfering with other dev tools + # We need to clear persistent cache, otherwise `deps.loadpaths` task fails with + # (Mix.Error) Can't continue due to errors on dependencies + # see https://github.com/elixir-lsp/elixir-ls/issues/120 + # originally reported in https://github.com/JakeBecker/elixir-ls/issues/71 + # Note that `Mix.State.clear_cache()` is not enough (at least on elixir 1.14) # FIXME: Private API - Mix.ProjectStack.post_config(build_path: ".elixir_ls/build") + Mix.Dep.clear_cached() + + Mix.Task.clear() # we need to reset compiler options # project may leave tracers after previous compilation and we don't woant them interfeering # see https://github.com/elixir-lsp/elixir-ls/issues/717 set_compiler_options() + # Override build directory to avoid interfering with other dev tools + # FIXME: Private API + Mix.ProjectStack.post_config(build_path: ".elixir_ls/build") + # We can get diagnostics if Mixfile fails to load {status, diagnostics} = case Kernel.ParallelCompiler.compile([mixfile]) do From 568ca1148d0340e8a6231dd590d1814e37b32409 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Tue, 11 Oct 2022 21:59:25 +0200 Subject: [PATCH 072/125] explicitely remove tracer in tests --- apps/language_server/test/server_test.exs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/apps/language_server/test/server_test.exs b/apps/language_server/test/server_test.exs index 2f78065f5..ec4ec933f 100644 --- a/apps/language_server/test/server_test.exs +++ b/apps/language_server/test/server_test.exs @@ -7,6 +7,12 @@ defmodule ElixirLS.LanguageServer.ServerTest do doctest(Server) + setup_all do + on_exit(fn -> + Code.put_compiler_option(:tracers, []) + end) + end + defp initialize(server) do Server.receive_packet(server, initialize_req(1, root_uri(), %{})) Server.receive_packet(server, notification("initialized")) From 67a195598a05a9d417baecf19fff3271fbbc8dbf Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Tue, 11 Oct 2022 21:59:37 +0200 Subject: [PATCH 073/125] run formatter --- .../lib/language_server/server.ex | 1 + .../lib/language_server/source_file.ex | 1 + .../test/source_file/invalid_project_test.exs | 25 +++++++++++-------- 3 files changed, 16 insertions(+), 11 deletions(-) diff --git a/apps/language_server/lib/language_server/server.ex b/apps/language_server/lib/language_server/server.ex index f81eadf3c..1e82cb30a 100644 --- a/apps/language_server/lib/language_server/server.ex +++ b/apps/language_server/lib/language_server/server.ex @@ -939,6 +939,7 @@ defmodule ElixirLS.LanguageServer.Server do defp trigger_build(state = %__MODULE__{project_dir: project_dir}) do build_automatically = Map.get(state.settings || %{}, "autoBuild", true) + cond do not build_enabled?(state) -> state diff --git a/apps/language_server/lib/language_server/source_file.ex b/apps/language_server/lib/language_server/source_file.ex index ad0b14db0..ea3e1a591 100644 --- a/apps/language_server/lib/language_server/source_file.ex +++ b/apps/language_server/lib/language_server/source_file.ex @@ -247,6 +247,7 @@ defmodule ElixirLS.LanguageServer.SourceFile do rescue e -> message = Exception.message(e) + IO.warn( "Unable to get formatter options for #{path}: #{inspect(e.__struct__)} #{message}" ) diff --git a/apps/language_server/test/source_file/invalid_project_test.exs b/apps/language_server/test/source_file/invalid_project_test.exs index a4ed16635..a83076d06 100644 --- a/apps/language_server/test/source_file/invalid_project_test.exs +++ b/apps/language_server/test/source_file/invalid_project_test.exs @@ -1,5 +1,4 @@ defmodule ElixirLS.LanguageServer.SourceFile.InvalidProjectTest do - use ExUnit.Case, async: false use Patch @@ -7,26 +6,30 @@ defmodule ElixirLS.LanguageServer.SourceFile.InvalidProjectTest do import ExUnit.CaptureIO describe "formatter_for " do - test "should handle syntax errors" do - patch(Mix.Tasks.Format, :formatter_for_file, fn _ -> raise %SyntaxError{file: ".formatter.exs", line: 1} end) - - output = capture_io(:stderr, fn -> - assert :error = SourceFile.formatter_for("file:///root.ex") + patch(Mix.Tasks.Format, :formatter_for_file, fn _ -> + raise %SyntaxError{file: ".formatter.exs", line: 1} end) + output = + capture_io(:stderr, fn -> + assert :error = SourceFile.formatter_for("file:///root.ex") + end) + assert String.contains?(output, "Unable to get formatter options") end test "should handle compile errors" do - patch(Mix.Tasks.Format, :formatter_for_file, fn _ -> raise %SyntaxError{file: ".formatter.exs", line: 1} end) - - output = capture_io(:stderr, fn -> - assert :error = SourceFile.formatter_for("file:///root.ex") + patch(Mix.Tasks.Format, :formatter_for_file, fn _ -> + raise %SyntaxError{file: ".formatter.exs", line: 1} end) + output = + capture_io(:stderr, fn -> + assert :error = SourceFile.formatter_for("file:///root.ex") + end) + assert String.contains?(output, "Unable to get formatter options") end - end end From 6fa8cb208dfadc84958c4e14d472d1c04f197011 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Tue, 11 Oct 2022 22:00:36 +0200 Subject: [PATCH 074/125] fix warnings --- .../lib/language_server/providers/document_symbols.ex | 2 +- apps/language_server/test/source_file_test.exs | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/language_server/lib/language_server/providers/document_symbols.ex b/apps/language_server/lib/language_server/providers/document_symbols.ex index ec8b47912..9ee3263de 100644 --- a/apps/language_server/lib/language_server/providers/document_symbols.ex +++ b/apps/language_server/lib/language_server/providers/document_symbols.ex @@ -61,7 +61,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbols do end # handle a bare defimpl, defprotocol or defmodule - defp extract_modules({defname, _, nil} = ast) + defp extract_modules({defname, _, nil}) when defname in [:defmodule, :defprotocol, :defimpl] do [] end diff --git a/apps/language_server/test/source_file_test.exs b/apps/language_server/test/source_file_test.exs index 1909e66aa..3c0937566 100644 --- a/apps/language_server/test/source_file_test.exs +++ b/apps/language_server/test/source_file_test.exs @@ -1,7 +1,6 @@ defmodule ElixirLS.LanguageServer.SourceFileTest do use ExUnit.Case, async: true use ExUnitProperties - import ElixirLS.LanguageServer.Test.PlatformTestHelpers alias ElixirLS.LanguageServer.SourceFile From d8642c7c5f1e9d2158f8ba43fb848a45a99e0df2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Samson?= Date: Thu, 13 Oct 2022 00:36:54 +0200 Subject: [PATCH 075/125] Logger backend for language server and fixes for debugger (#746) * console backend forked from elixir * rename * rename * remove colors * remove buffering * remove device * replace console logger fix errors * run formatter * remove tests * consistently use Logger for ordinary logging * crash instead of warning * use logger * attempt to show error message before crashing Fixes https://github.com/elixir-lsp/elixir-ls/issues/741 * info->warn * exit server when config changes it will be restarted by the client * fix tests * use new function in debugger * use DAP compliant output category in debugger stdout and stderr is reserved for debuggee, console and important for debugger * set group leader to logger backend in tests * fix tests --- apps/elixir_ls_debugger/lib/debugger.ex | 5 +- .../lib/debugger/breakpoint_condition.ex | 15 +- apps/elixir_ls_debugger/lib/debugger/cli.ex | 19 +- .../elixir_ls_debugger/lib/debugger/output.ex | 12 +- .../elixir_ls_debugger/lib/debugger/server.ex | 47 ++--- .../lib/debugger/stacktrace.ex | 6 +- .../elixir_ls_debugger/test/debugger_test.exs | 30 ++-- apps/elixir_ls_utils/lib/launch.ex | 15 +- apps/elixir_ls_utils/lib/packet_stream.ex | 2 +- .../test/packet_stream_test.exs | 62 ++++--- .../test/support/packet_capture.ex | 1 - .../lib/language_server/build.ex | 17 +- .../lib/language_server/cli.ex | 57 +++++- .../lib/language_server/dialyzer.ex | 21 +-- .../lib/language_server/dialyzer/analyzer.ex | 7 +- .../lib/language_server/dialyzer/manifest.ex | 8 +- .../json_rpc_logger_backend.ex | 162 ++++++++++++++++++ .../providers/workspace_symbols.ex | 18 +- .../lib/language_server/server.ex | 65 ++++--- .../lib/language_server/source_file.ex | 3 +- .../test/providers/workspace_symbols_test.exs | 5 - .../test/source_file/invalid_project_test.exs | 6 +- .../test/support/server_test_helpers.ex | 25 +++ 23 files changed, 428 insertions(+), 180 deletions(-) create mode 100644 apps/language_server/lib/language_server/json_rpc_logger_backend.ex diff --git a/apps/elixir_ls_debugger/lib/debugger.ex b/apps/elixir_ls_debugger/lib/debugger.ex index 09484cf0a..64d253ba5 100644 --- a/apps/elixir_ls_debugger/lib/debugger.ex +++ b/apps/elixir_ls_debugger/lib/debugger.ex @@ -4,12 +4,13 @@ defmodule ElixirLS.Debugger do """ use Application + alias ElixirLS.Debugger.Output @impl Application def start(_type, _args) do # We don't start this as a worker because if the debugger crashes, we want # this process to remain alive to print errors - {:ok, _pid} = ElixirLS.Debugger.Output.start(ElixirLS.Debugger.Output) + {:ok, _pid} = Output.start(Output) children = [ {ElixirLS.Debugger.Server, name: ElixirLS.Debugger.Server} @@ -22,7 +23,7 @@ defmodule ElixirLS.Debugger do @impl Application def stop(_state) do if ElixirLS.Utils.WireProtocol.io_intercepted?() do - IO.puts(:standard_error, "ElixirLS debugger has crashed") + Output.debugger_important("ElixirLS debugger has crashed") :init.stop(1) end diff --git a/apps/elixir_ls_debugger/lib/debugger/breakpoint_condition.ex b/apps/elixir_ls_debugger/lib/debugger/breakpoint_condition.ex index 10ab34b58..27c3639ca 100644 --- a/apps/elixir_ls_debugger/lib/debugger/breakpoint_condition.ex +++ b/apps/elixir_ls_debugger/lib/debugger/breakpoint_condition.ex @@ -4,6 +4,7 @@ defmodule ElixirLS.Debugger.BreakpointCondition do """ use GenServer + alias ElixirLS.Debugger.Output @range 0..99 def start_link(args) do @@ -162,7 +163,7 @@ defmodule ElixirLS.Debugger.BreakpointCondition do # Debug Adapter Protocol: # If this attribute exists and is non-empty, the backend must not 'break' (stop) # but log the message instead. Expressions within {} are interpolated. - IO.puts(interpolate(log_message, elixir_binding)) + Output.debugger_console(interpolate(log_message, elixir_binding)) false else result @@ -178,7 +179,10 @@ defmodule ElixirLS.Debugger.BreakpointCondition do if term, do: true, else: false catch kind, error -> - IO.warn("Error in conditional breakpoint: " <> Exception.format_banner(kind, error)) + Output.debugger_important( + "Error in conditional breakpoint: " <> Exception.format_banner(kind, error) + ) + false end end @@ -189,7 +193,10 @@ defmodule ElixirLS.Debugger.BreakpointCondition do to_string(term) catch kind, error -> - IO.warn("Error in log message interpolation: " <> Exception.format_banner(kind, error)) + Output.debugger_important( + "Error in log message interpolation: " <> Exception.format_banner(kind, error) + ) + "" end end @@ -220,7 +227,7 @@ defmodule ElixirLS.Debugger.BreakpointCondition do interpolate(expression_rest, [eval_result | acc], elixir_binding) :error -> - IO.warn("Log message has unpaired or nested `{}`") + Output.debugger_important("Log message has unpaired or nested `{}`") acc end end diff --git a/apps/elixir_ls_debugger/lib/debugger/cli.ex b/apps/elixir_ls_debugger/lib/debugger/cli.ex index bf019f9a5..c995fe37c 100644 --- a/apps/elixir_ls_debugger/lib/debugger/cli.ex +++ b/apps/elixir_ls_debugger/lib/debugger/cli.ex @@ -3,12 +3,21 @@ defmodule ElixirLS.Debugger.CLI do alias ElixirLS.Debugger.{Output, Server} def main do - WireProtocol.intercept_output(&Output.print/1, &Output.print_err/1) + WireProtocol.intercept_output(&Output.debuggee_out/1, &Output.debuggee_err/1) Launch.start_mix() {:ok, _} = Application.ensure_all_started(:elixir_ls_debugger, :permanent) - IO.puts("Started ElixirLS debugger v#{Launch.debugger_version()}") - Launch.print_versions() + Output.debugger_console("Started ElixirLS Debugger v#{Launch.debugger_version()}") + versions = Launch.get_versions() + + Output.debugger_console( + "ElixirLS Debugger built with elixir #{versions.compile_elixir_version} on OTP #{versions.compile_otp_version}" + ) + + Output.debugger_console( + "Running on elixir #{versions.current_elixir_version} on OTP #{versions.current_otp_version}" + ) + Launch.limit_num_schedulers() warn_if_unsupported_version() WireProtocol.stream_packets(&Server.receive_packet/1) @@ -16,11 +25,11 @@ defmodule ElixirLS.Debugger.CLI do defp warn_if_unsupported_version do with {:error, message} <- ElixirLS.Utils.MinimumVersion.check_elixir_version() do - Output.print_err("WARNING: " <> message) + Output.debugger_important("WARNING: " <> message) end with {:error, message} <- ElixirLS.Utils.MinimumVersion.check_otp_version() do - Output.print_err("WARNING: " <> message) + Output.debugger_important("WARNING: " <> message) end end end diff --git a/apps/elixir_ls_debugger/lib/debugger/output.ex b/apps/elixir_ls_debugger/lib/debugger/output.ex index bd1481c37..ca19e4db1 100644 --- a/apps/elixir_ls_debugger/lib/debugger/output.ex +++ b/apps/elixir_ls_debugger/lib/debugger/output.ex @@ -29,11 +29,19 @@ defmodule ElixirLS.Debugger.Output do GenServer.call(server, {:send_event, event, body}) end - def print(server \\ __MODULE__, str) when is_binary(str) do + def debugger_console(server \\ __MODULE__, str) when is_binary(str) do + send_event(server, "output", %{"category" => "console", "output" => str}) + end + + def debugger_important(server \\ __MODULE__, str) when is_binary(str) do + send_event(server, "output", %{"category" => "important", "output" => str}) + end + + def debuggee_out(server \\ __MODULE__, str) when is_binary(str) do send_event(server, "output", %{"category" => "stdout", "output" => str}) end - def print_err(server \\ __MODULE__, str) when is_binary(str) do + def debuggee_err(server \\ __MODULE__, str) when is_binary(str) do send_event(server, "output", %{"category" => "stderr", "output" => str}) end diff --git a/apps/elixir_ls_debugger/lib/debugger/server.ex b/apps/elixir_ls_debugger/lib/debugger/server.ex index 50ab16250..7cea4e2f5 100644 --- a/apps/elixir_ls_debugger/lib/debugger/server.ex +++ b/apps/elixir_ls_debugger/lib/debugger/server.ex @@ -166,8 +166,7 @@ defmodule ElixirLS.Debugger.Server do 0 _ -> - IO.puts( - :standard_error, + Output.debugger_important( "(Debugger) Task failed because " <> Exception.format_exit(reason) ) @@ -181,8 +180,7 @@ defmodule ElixirLS.Debugger.Server do end def handle_info({:DOWN, _ref, :process, pid, reason}, state = %__MODULE__{}) do - IO.puts( - :standard_error, + Output.debugger_important( "debugged process #{inspect(pid)} exited with reason #{Exception.format_exit(reason)}" ) @@ -222,7 +220,7 @@ defmodule ElixirLS.Debugger.Server do @impl GenServer def terminate(reason, _state = %__MODULE__{}) do if reason != :normal do - IO.puts(:standard_error, "(Debugger) Terminating because #{Exception.format_exit(reason)}") + Output.debugger_important("(Debugger) Terminating because #{Exception.format_exit(reason)}") end end @@ -231,17 +229,17 @@ defmodule ElixirLS.Debugger.Server do defp handle_request(initialize_req(_, client_info), %__MODULE__{client_info: nil} = state) do # linesStartAt1 is true by default and we only support 1-based indexing if client_info["linesStartAt1"] == false do - IO.warn("0-based lines are not supported") + Output.debugger_important("0-based lines are not supported") end # columnsStartAt1 is true by default and we only support 1-based indexing if client_info["columnsStartAt1"] == false do - IO.warn("0-based columns are not supported") + Output.debugger_important("0-based columns are not supported") end # pathFormat is `path` by default and we do not support other, e.g. `uri` if client_info["pathFormat"] not in [nil, "path"] do - IO.warn("pathFormat #{client_info["pathFormat"]} not supported") + Output.debugger_important("pathFormat #{client_info["pathFormat"]} not supported") end {capabilities(), %{state | client_info: client_info}} @@ -258,7 +256,7 @@ defmodule ElixirLS.Debugger.Server do defp handle_request(launch_req(_, config) = args, state = %__MODULE__{}) do if args["arguments"]["noDebug"] == true do - IO.warn("launch with no debug is not supported") + Output.debugger_important("launch with no debug is not supported") end {_, ref} = spawn_monitor(fn -> initialize(config) end) @@ -266,8 +264,7 @@ defmodule ElixirLS.Debugger.Server do receive do {:DOWN, ^ref, :process, _pid, reason} -> if reason != :normal do - IO.puts( - :standard_error, + Output.debugger_important( "(Debugger) Initialization failed because " <> Exception.format_exit(reason) ) @@ -337,7 +334,9 @@ defmodule ElixirLS.Debugger.Server do :ok {:error, :function_not_found} -> - IO.warn("Unable to delete function breakpoint on #{inspect({m, f, a})}") + Output.debugger_important( + "Unable to delete function breakpoint on #{inspect({m, f, a})}" + ) end end @@ -735,7 +734,9 @@ defmodule ElixirLS.Debugger.Server do catch kind, payload -> # when stepping out of interpreted code a MatchError is risen inside :int module (at least in OTP 23) - IO.warn(":int.#{action}(#{inspect(pid)}) failed: #{Exception.format(kind, payload)}") + Output.debugger_important( + ":int.#{action}(#{inspect(pid)}) failed: #{Exception.format(kind, payload)}" + ) unless action == :continue do safe_int_action(pid, :continue) @@ -972,7 +973,7 @@ defmodule ElixirLS.Debugger.Server do unless is_list(task_args) and "--no-compile" in task_args do case Mix.Task.run("compile", ["--ignore-module-conflict"]) do {:error, _} -> - IO.puts(:standard_error, "Aborting debugger due to compile errors") + Output.debugger_important("Aborting debugger due to compile errors") :init.stop(1) _ -> @@ -1020,7 +1021,7 @@ defmodule ElixirLS.Debugger.Server do defp set_stack_trace_mode(nil), do: nil defp set_stack_trace_mode(_) do - IO.warn(~S(stackTraceMode must be "all", "no_tail", or "false")) + Output.debugger_important(~S(stackTraceMode must be "all", "no_tail", or "false")) end defp capabilities do @@ -1150,8 +1151,7 @@ defmodule ElixirLS.Debugger.Server do [regex] {:error, error} -> - IO.puts( - :standard_error, + Output.debugger_important( "Unable to compile file pattern (#{inspect(pattern)}) into a regex. Received error: #{inspect(error)}" ) @@ -1226,7 +1226,7 @@ defmodule ElixirLS.Debugger.Server do {:module, _} = :int.ni(mod) catch _, _ -> - IO.warn( + Output.debugger_important( "Module #{inspect(mod)} cannot be interpreted. Consider adding it to `excludeModules`." ) end @@ -1252,7 +1252,7 @@ defmodule ElixirLS.Debugger.Server do end {:error, reason} -> - IO.warn( + Output.debugger_important( "Unable to set condition on a breakpoint in #{module}:#{inspect(lines)}: #{inspect(reason)}" ) end @@ -1266,7 +1266,7 @@ defmodule ElixirLS.Debugger.Server do condition {:error, reason} -> - IO.warn("Cannot parse breakpoint condition: #{inspect(reason)}") + Output.debugger_important("Cannot parse breakpoint condition: #{inspect(reason)}") "true" end end @@ -1280,12 +1280,15 @@ defmodule ElixirLS.Debugger.Server do if is_integer(term) do term else - IO.warn("Hit condition must evaluate to integer") + Output.debugger_important("Hit condition must evaluate to integer") 0 end catch kind, error -> - IO.warn("Error while evaluating hit condition: " <> Exception.format_banner(kind, error)) + Output.debugger_important( + "Error while evaluating hit condition: " <> Exception.format_banner(kind, error) + ) + 0 end end diff --git a/apps/elixir_ls_debugger/lib/debugger/stacktrace.ex b/apps/elixir_ls_debugger/lib/debugger/stacktrace.ex index efbf20299..549a5b3da 100644 --- a/apps/elixir_ls_debugger/lib/debugger/stacktrace.ex +++ b/apps/elixir_ls_debugger/lib/debugger/stacktrace.ex @@ -2,6 +2,7 @@ defmodule ElixirLS.Debugger.Stacktrace do @moduledoc """ Retrieves the stack trace for a process that's paused at a breakpoint """ + alias ElixirLS.Debugger.Output defmodule Frame do defstruct [:level, :file, :module, :function, :args, :line, :bindings, :messages] @@ -56,7 +57,10 @@ defmodule ElixirLS.Debugger.Stacktrace do [first_frame | other_frames] error -> - IO.warn("Failed to obtain meta for pid #{inspect(pid)}: #{inspect(error)}") + Output.debugger_important( + "Failed to obtain meta for pid #{inspect(pid)}: #{inspect(error)}" + ) + [] end end diff --git a/apps/elixir_ls_debugger/test/debugger_test.exs b/apps/elixir_ls_debugger/test/debugger_test.exs index 0f83e5df6..434cd3041 100644 --- a/apps/elixir_ls_debugger/test/debugger_test.exs +++ b/apps/elixir_ls_debugger/test/debugger_test.exs @@ -402,7 +402,7 @@ defmodule ElixirLS.Debugger.ServerTest do }), 500 - {log, stderr} = + {log, _stderr} = capture_log_and_io(:standard_error, fn -> assert_receive event(_, "thread", %{ "reason" => "exited", @@ -412,7 +412,6 @@ defmodule ElixirLS.Debugger.ServerTest do end) assert log =~ "Fixture MixProject expected error" - assert stderr =~ "Fixture MixProject expected error" end) end @@ -461,7 +460,7 @@ defmodule ElixirLS.Debugger.ServerTest do }), 5000 - {log, io} = + {log, _io} = capture_log_and_io(:stderr, fn -> assert_receive event(_, "thread", %{ "reason" => "exited", @@ -471,7 +470,6 @@ defmodule ElixirLS.Debugger.ServerTest do end) assert log =~ "Fixture MixProject raise for exit_self/0" - assert io =~ "Fixture MixProject raise for exit_self/0" assert_receive event(_, "exited", %{ "exitCode" => 1 @@ -560,20 +558,20 @@ defmodule ElixirLS.Debugger.ServerTest do |> Enum.filter(&(&1["name"] |> String.starts_with?("MixProject.Some"))) |> Enum.map(& &1["id"]) - {_, stderr} = - capture_log_and_io(:standard_error, fn -> - Server.receive_packet(server, request(7, "pause", %{"threadId" => thread_id})) - assert_receive(response(_, 7, "pause", %{}), 500) + Server.receive_packet(server, request(7, "pause", %{"threadId" => thread_id})) + assert_receive(response(_, 7, "pause", %{}), 500) - assert_receive event(_, "stopped", %{ - "allThreadsStopped" => false, - "reason" => "pause", - "threadId" => ^thread_id - }), - 500 - end) + assert_receive event(_, "stopped", %{ + "allThreadsStopped" => false, + "reason" => "pause", + "threadId" => ^thread_id + }), + 500 - assert stderr =~ "Failed to obtain meta for pid" + assert_receive event(_, "output", %{ + "category" => "important", + "output" => "Failed to obtain meta for pid" <> _ + }) end) end diff --git a/apps/elixir_ls_utils/lib/launch.ex b/apps/elixir_ls_utils/lib/launch.ex index d5c8ae83a..ffc046ad6 100644 --- a/apps/elixir_ls_utils/lib/launch.ex +++ b/apps/elixir_ls_utils/lib/launch.ex @@ -13,14 +13,13 @@ defmodule ElixirLS.Utils.Launch do :ok end - def print_versions do - IO.inspect(System.build_info()[:build], label: "Elixir version") - IO.inspect(System.otp_release(), label: "Erlang version") - - IO.puts( - "ElixirLS compiled with Elixir #{@compiled_elixir_version}" <> - " and erlang #{@compiled_otp_version}" - ) + def get_versions do + %{ + current_elixir_version: inspect(System.build_info()[:build]), + current_otp_version: inspect(System.otp_release()), + compile_elixir_version: inspect(@compiled_elixir_version), + compile_otp_version: inspect(@compiled_otp_version) + } end def language_server_version do diff --git a/apps/elixir_ls_utils/lib/packet_stream.ex b/apps/elixir_ls_utils/lib/packet_stream.ex index 17017e414..1201f9834 100644 --- a/apps/elixir_ls_utils/lib/packet_stream.ex +++ b/apps/elixir_ls_utils/lib/packet_stream.ex @@ -31,7 +31,7 @@ defmodule ElixirLS.Utils.PacketStream do :ok {:error, reason} -> - IO.warn("Unable to read from device: #{inspect(reason)}") + raise "Unable to read from device: #{inspect(reason)}" end ) end diff --git a/apps/elixir_ls_utils/test/packet_stream_test.exs b/apps/elixir_ls_utils/test/packet_stream_test.exs index 15183718a..8e47ca7b7 100644 --- a/apps/elixir_ls_utils/test/packet_stream_test.exs +++ b/apps/elixir_ls_utils/test/packet_stream_test.exs @@ -2,7 +2,6 @@ defmodule ElixirLS.Utils.PacketStreamTest do use ExUnit.Case, async: true alias ElixirLS.Utils.PacketStream - import ExUnit.CaptureIO describe "content-type" do test "default mime" do @@ -123,11 +122,14 @@ defmodule ElixirLS.Utils.PacketStreamTest do {:ok, pid} = File.open("test/fixtures/protocol_messages/invalid_content_length", [:read, :binary]) - assert capture_io(:stderr, fn -> - assert [] = - PacketStream.stream(pid) - |> Enum.to_list() - end) =~ "Unable to read from device: :truncated" + error = + assert_raise RuntimeError, fn -> + assert [] = + PacketStream.stream(pid) + |> Enum.to_list() + end + + assert error.message =~ "Unable to read from device: :truncated" File.close(pid) end @@ -136,11 +138,14 @@ defmodule ElixirLS.Utils.PacketStreamTest do {:ok, pid} = File.open("test/fixtures/protocol_messages/invalid_content_type", [:read, :binary]) - assert capture_io(:stderr, fn -> - assert [] = - PacketStream.stream(pid) - |> Enum.to_list() - end) =~ "Unable to read from device: :not_supported_content_type" + error = + assert_raise RuntimeError, fn -> + assert [] = + PacketStream.stream(pid) + |> Enum.to_list() + end + + assert error.message =~ "Unable to read from device: :not_supported_content_type" File.close(pid) end @@ -149,11 +154,14 @@ defmodule ElixirLS.Utils.PacketStreamTest do for i <- 0..6 do {:ok, pid} = File.open("test/fixtures/protocol_messages/no_body_#{i}", [:read, :binary]) - capture_io(:stderr, fn -> + try do assert [] = PacketStream.stream(pid) |> Enum.to_list() - end) + rescue + RuntimeError -> + :ok + end File.close(pid) end @@ -162,11 +170,14 @@ defmodule ElixirLS.Utils.PacketStreamTest do test "invalid JSON" do {:ok, pid} = File.open("test/fixtures/protocol_messages/invalid_json", [:read, :binary]) - assert capture_io(:stderr, fn -> - assert [] = - PacketStream.stream(pid) - |> Enum.to_list() - end) =~ "Unable to read from device: %JasonVendored.DecodeError" + error = + assert_raise RuntimeError, fn -> + assert [] = + PacketStream.stream(pid) + |> Enum.to_list() + end + + assert error.message =~ "Unable to read from device: %JasonVendored.DecodeError" File.close(pid) end @@ -175,12 +186,15 @@ defmodule ElixirLS.Utils.PacketStreamTest do {:ok, pid} = File.open("test/fixtures/protocol_messages/invalid_after_valid", [:read, :binary]) - assert capture_io(:stderr, fn -> - # note that we halt the stream and discard any further valid messages - assert [%{"some" => "value"}] = - PacketStream.stream(pid) - |> Enum.to_list() - end) =~ "Unable to read from device: %JasonVendored.DecodeError" + error = + assert_raise RuntimeError, fn -> + # note that we halt the stream and discard any further valid messages + assert [%{"some" => "value"}] = + PacketStream.stream(pid) + |> Enum.to_list() + end + + assert error.message =~ "Unable to read from device: %JasonVendored.DecodeError" File.close(pid) end diff --git a/apps/elixir_ls_utils/test/support/packet_capture.ex b/apps/elixir_ls_utils/test/support/packet_capture.ex index 24166754e..265447055 100644 --- a/apps/elixir_ls_utils/test/support/packet_capture.ex +++ b/apps/elixir_ls_utils/test/support/packet_capture.ex @@ -26,7 +26,6 @@ defmodule ElixirLS.Utils.PacketCapture do handle_output(to_string(chars), from, reply_as, parent) end - @impl GenServer def handle_info( {:io_request, from, reply_as, {:put_chars, _encoding, module, fun, args}}, parent diff --git a/apps/language_server/lib/language_server/build.ex b/apps/language_server/lib/language_server/build.ex index bd7dad542..eb3824c1e 100644 --- a/apps/language_server/lib/language_server/build.ex +++ b/apps/language_server/lib/language_server/build.ex @@ -1,18 +1,18 @@ defmodule ElixirLS.LanguageServer.Build do alias ElixirLS.LanguageServer.{Server, JsonRpc, Diagnostics, Tracer} alias ElixirLS.Utils.MixfileHelpers + require Logger def build(parent, root_path, opts) when is_binary(root_path) do if Path.absname(File.cwd!()) != Path.absname(root_path) do - IO.puts("Skipping build because cwd changed from #{root_path} to #{File.cwd!()}") + Logger.info("Skipping build because cwd changed from #{root_path} to #{File.cwd!()}") {nil, nil} else spawn_monitor(fn -> with_build_lock(fn -> {us, _} = :timer.tc(fn -> - IO.puts("MIX_ENV: #{Mix.env()}") - IO.puts("MIX_TARGET: #{Mix.target()}") + Logger.info("Starting build with MIX_ENV: #{Mix.env()} MIX_TARGET: #{Mix.target()}") case reload_project() do {:ok, mixfile_diagnostics} -> @@ -41,7 +41,7 @@ defmodule ElixirLS.LanguageServer.Build do end) Tracer.save() - JsonRpc.log_message(:info, "Compile took #{div(us, 1000)} milliseconds") + Logger.info("Compile took #{div(us, 1000)} milliseconds") end) end) end @@ -114,7 +114,7 @@ defmodule ElixirLS.LanguageServer.Build do "No mixfile found in project. " <> "To use a subdirectory, set `elixirLS.projectDir` in your settings" - JsonRpc.log_message(:info, msg <> ". Looked for mixfile at #{inspect(mixfile)}") + Logger.warn(msg <> ". Looked for mixfile at #{inspect(mixfile)}") :no_mixfile end @@ -149,7 +149,7 @@ defmodule ElixirLS.LanguageServer.Build do if Enum.all?(results, &match?(:ok, &1)) do :ok else - JsonRpc.log_message(:error, "mix clean returned #{inspect(results)}") + Logger.error("mix clean returned #{inspect(results)}") {:error, :clean_failed} end end @@ -166,10 +166,7 @@ defmodule ElixirLS.LanguageServer.Build do :ok {:error, reason} -> - JsonRpc.log_message( - :warning, - "Unable to purge consolidated protocols from #{path}: #{inspect(reason)}" - ) + Logger.warn("Unable to purge consolidated protocols from #{path}: #{inspect(reason)}") end # NOTE this implementation is based on https://github.com/phoenixframework/phoenix/commit/b5580e9 diff --git a/apps/language_server/lib/language_server/cli.ex b/apps/language_server/lib/language_server/cli.ex index f747e5841..748e1b39d 100644 --- a/apps/language_server/lib/language_server/cli.ex +++ b/apps/language_server/lib/language_server/cli.ex @@ -2,17 +2,42 @@ defmodule ElixirLS.LanguageServer.CLI do alias ElixirLS.Utils.{WireProtocol, Launch} alias ElixirLS.LanguageServer.JsonRpc alias ElixirLS.LanguageServer.Build + require Logger def main do WireProtocol.intercept_output(&JsonRpc.print/1, &JsonRpc.print_err/1) + + # :logger application is already started + # replace console logger with LSP + Application.put_env(:logger, :backends, [Logger.Backends.JsonRpc]) + + Application.put_env(:logger, Logger.Backends.JsonRpc, + level: :debug, + format: "$message", + metadata: [] + ) + + {:ok, _} = Logger.add_backend(Logger.Backends.JsonRpc) + :ok = Logger.remove_backend(:console, flush: true) + Launch.start_mix() Build.set_compiler_options() start_language_server() - IO.puts("Started ElixirLS v#{Launch.language_server_version()}") - Launch.print_versions() + Logger.info("Started ElixirLS v#{Launch.language_server_version()}") + + versions = Launch.get_versions() + + Logger.info( + "ElixirLS built with elixir #{versions.compile_elixir_version} on OTP #{versions.compile_otp_version}" + ) + + Logger.info( + "Running on elixir #{versions.current_elixir_version} on OTP #{versions.current_otp_version}" + ) + Launch.limit_num_schedulers() Mix.shell(ElixirLS.LanguageServer.MixShell) @@ -22,25 +47,39 @@ defmodule ElixirLS.LanguageServer.CLI do WireProtocol.stream_packets(&JsonRpc.receive_packet/1) end - defp start_language_server do + defp incomplete_installation_message(hash \\ "") do guide = "https://github.com/elixir-lsp/elixir-ls/blob/master/guides/incomplete-installation.md" + "Unable to start ElixirLS due to an incomplete erlang installation. " <> + "See #{guide}#{hash} for guidance." + end + + defp start_language_server do case Application.ensure_all_started(:language_server, :temporary) do {:ok, _} -> :ok {:error, {:edoc, {'no such file or directory', 'edoc.app'}}} -> - raise "Unable to start ElixirLS due to an incomplete erlang installation. " <> - "See #{guide}#edoc-missing for guidance." + message = incomplete_installation_message("#edoc-missing") + + JsonRpc.show_message(:error, message) + Process.sleep(5000) + raise message {:error, {:dialyzer, {'no such file or directory', 'dialyzer.app'}}} -> - raise "Unable to start ElixirLS due to an incomplete erlang installation. " <> - "See #{guide}#dialyzer-missing for guidance." + message = incomplete_installation_message("#dialyzer-missing") + + JsonRpc.show_message(:error, message) + Process.sleep(5000) + raise message {:error, _} -> - raise "Unable to start ElixirLS due to an incomplete erlang installation. " <> - "See #{guide} for guidance." + message = incomplete_installation_message() + + JsonRpc.show_message(:error, message) + Process.sleep(5000) + raise message end end end diff --git a/apps/language_server/lib/language_server/dialyzer.ex b/apps/language_server/lib/language_server/dialyzer.ex index 48b9ffca3..24feeb2ed 100644 --- a/apps/language_server/lib/language_server/dialyzer.ex +++ b/apps/language_server/lib/language_server/dialyzer.ex @@ -3,6 +3,7 @@ defmodule ElixirLS.LanguageServer.Dialyzer do alias ElixirLS.LanguageServer.Dialyzer.{Manifest, Analyzer, Utils, SuccessTypings} import Utils use GenServer + require Logger defstruct [ :parent, @@ -147,7 +148,7 @@ defmodule ElixirLS.LanguageServer.Dialyzer do state = ElixirLS.LanguageServer.Build.with_build_lock(fn -> if Mix.Project.get() do - JsonRpc.log_message(:info, "[ElixirLS Dialyzer] Checking for stale beam files") + Logger.info("[ElixirLS Dialyzer] Checking for stale beam files") new_timestamp = adjusted_timestamp() {removed_files, file_changes} = @@ -261,8 +262,7 @@ defmodule ElixirLS.LanguageServer.Dialyzer do {changed, changed_contents} end) - JsonRpc.log_message( - :info, + Logger.info( "[ElixirLS Dialyzer] Found #{length(changed)} changed files in #{div(us, 1000)} milliseconds" ) @@ -338,7 +338,7 @@ defmodule ElixirLS.LanguageServer.Dialyzer do :ok {:error, reason} -> - IO.warn("Unable to remove temporary file #{path}: #{inspect(reason)}") + Logger.warn("Unable to remove temporary file #{path}: #{inspect(reason)}") end end @@ -375,8 +375,7 @@ defmodule ElixirLS.LanguageServer.Dialyzer do warnings = Map.drop(warnings, modules_to_analyze) # Analyze! - JsonRpc.log_message( - :info, + Logger.info( "[ElixirLS Dialyzer] Analyzing #{Enum.count(modules_to_analyze)} modules: " <> "#{inspect(modules_to_analyze)}" ) @@ -396,10 +395,7 @@ defmodule ElixirLS.LanguageServer.Dialyzer do {active_plt, mod_deps, md5, warnings} end) - JsonRpc.log_message( - :info, - "[ElixirLS Dialyzer] Analysis finished in #{div(us, 1000)} milliseconds" - ) + Logger.info("[ElixirLS Dialyzer] Analysis finished in #{div(us, 1000)} milliseconds") analysis_finished(parent, :ok, active_plt, mod_deps, md5, warnings, timestamp, build_ref) end @@ -507,7 +503,7 @@ defmodule ElixirLS.LanguageServer.Dialyzer do end defp normalize_postion(position) do - IO.warn("dialyzer returned warning with invalid position #{inspect(position)}") + Logger.warn("dialyzer returned warning with invalid position #{inspect(position)}") 0 end @@ -534,8 +530,7 @@ defmodule ElixirLS.LanguageServer.Dialyzer do end defp warning_message(raw_warning, warning_format) do - JsonRpc.log_message( - :info, + Logger.info( "[ElixirLS Dialyzer] Unrecognized dialyzerFormat setting: #{inspect(warning_format)}" <> ", falling back to \"dialyzer\"" ) diff --git a/apps/language_server/lib/language_server/dialyzer/analyzer.ex b/apps/language_server/lib/language_server/dialyzer/analyzer.ex index 5054ef776..0d8e5dc79 100644 --- a/apps/language_server/lib/language_server/dialyzer/analyzer.ex +++ b/apps/language_server/lib/language_server/dialyzer/analyzer.ex @@ -1,5 +1,6 @@ defmodule ElixirLS.LanguageServer.Dialyzer.Analyzer do require Record + require Logger # warn_race_condition is unsupported because it greatly increases analysis time # OTP 25 dropped support for warn_race_condition @@ -178,7 +179,9 @@ defmodule ElixirLS.LanguageServer.Dialyzer.Analyzer do end defp print_failure(reason, log_cache) do - IO.puts("Analysis failed: " <> Exception.format_exit(reason)) - for msg <- log_cache, do: IO.puts(msg) + message = + "Analysis failed: " <> Exception.format_exit(reason) <> "\n" <> Enum.join(log_cache, "\n") + + Logger.error(message) end end diff --git a/apps/language_server/lib/language_server/dialyzer/manifest.ex b/apps/language_server/lib/language_server/dialyzer/manifest.ex index d045cc350..69f56a042 100644 --- a/apps/language_server/lib/language_server/dialyzer/manifest.ex +++ b/apps/language_server/lib/language_server/dialyzer/manifest.ex @@ -2,6 +2,7 @@ defmodule ElixirLS.LanguageServer.Dialyzer.Manifest do alias ElixirLS.LanguageServer.{Dialyzer, Dialyzer.Utils, JsonRpc} import Record import Dialyzer.Utils + require Logger @manifest_vsn :v2 @@ -50,7 +51,7 @@ defmodule ElixirLS.LanguageServer.Dialyzer.Manifest do # Because the manifest file can be several megabytes, we do a write-then-rename # to reduce the likelihood of corrupting the manifest - JsonRpc.log_message(:info, "[ElixirLS Dialyzer] Writing manifest...") + Logger.info("[ElixirLS Dialyzer] Writing manifest...") File.mkdir_p!(Path.dirname(manifest_path)) tmp_manifest_path = manifest_path <> ".new" File.write!(tmp_manifest_path, :erlang.term_to_binary(manifest_data, compressed: 1)) @@ -58,10 +59,7 @@ defmodule ElixirLS.LanguageServer.Dialyzer.Manifest do File.touch!(manifest_path, timestamp) end) - JsonRpc.log_message( - :info, - "[ElixirLS Dialyzer] Done writing manifest in #{div(us, 1000)} milliseconds." - ) + Logger.info("[ElixirLS Dialyzer] Done writing manifest in #{div(us, 1000)} milliseconds.") end) end diff --git a/apps/language_server/lib/language_server/json_rpc_logger_backend.ex b/apps/language_server/lib/language_server/json_rpc_logger_backend.ex new file mode 100644 index 000000000..a8db91d93 --- /dev/null +++ b/apps/language_server/lib/language_server/json_rpc_logger_backend.ex @@ -0,0 +1,162 @@ +defmodule Logger.Backends.JsonRpc do + @moduledoc ~S""" + A logger backend that logs messages by sending them via LSP ‘window/logMessage’. + + ## Options + + * `:level` - the level to be logged by this backend. + Note that messages are filtered by the general + `:level` configuration for the `:logger` application first. + + * `:format` - the format message used to print logs. + Defaults to: `"$message"`. + It may also be a `{module, function}` tuple that is invoked + with the log level, the message, the current timestamp and + the metadata and must return `t:IO.chardata/0`. See + `Logger.Formatter`. + + * `:metadata` - the metadata to be printed by `$metadata`. + Defaults to an empty list (no metadata). + Setting `:metadata` to `:all` prints all metadata. See + the "Metadata" section for more information. + + """ + + @behaviour :gen_event + + defstruct format: nil, + level: nil, + metadata: nil + + @impl true + def init(__MODULE__) do + config = Application.get_env(:logger, __MODULE__) + + {:ok, init(config, %__MODULE__{})} + end + + def init({__MODULE__, opts}) when is_list(opts) do + config = configure_merge(Application.get_env(:logger, __MODULE__), opts) + {:ok, init(config, %__MODULE__{})} + end + + @impl true + def handle_call({:configure, options}, state) do + {:ok, :ok, configure(options, state)} + end + + def handle_call({:set_group_leader, pid}, state) do + Process.group_leader(self(), pid) + {:ok, :ok, state} + end + + @impl true + def handle_event({level, _gl, {Logger, msg, ts, md}}, state) do + %{level: log_level} = state + + {:erl_level, level} = List.keyfind(md, :erl_level, 0, {:erl_level, level}) + + cond do + not meet_level?(level, log_level) -> + {:ok, state} + + true -> + {:ok, log_event(level, msg, ts, md, state)} + end + end + + def handle_event(:flush, state) do + {:ok, state} + end + + def handle_event(_, state) do + {:ok, state} + end + + @impl true + def handle_info(_, state) do + {:ok, state} + end + + @impl true + def code_change(_old_vsn, state, _extra) do + {:ok, state} + end + + @impl true + def terminate(_reason, _state) do + :ok + end + + ## Helpers + + defp meet_level?(_lvl, nil), do: true + + defp meet_level?(lvl, min) do + Logger.compare_levels(lvl, min) != :lt + end + + defp configure(options, state) do + config = configure_merge(Application.get_env(:logger, __MODULE__), options) + Application.put_env(:logger, __MODULE__, config) + init(config, state) + end + + defp init(config, state) do + level = Keyword.get(config, :level) + format = Logger.Formatter.compile(Keyword.get(config, :format)) + metadata = Keyword.get(config, :metadata, []) |> configure_metadata() + + %{ + state + | format: format, + metadata: metadata, + level: level + } + end + + defp configure_metadata(:all), do: :all + defp configure_metadata(metadata), do: Enum.reverse(metadata) + + defp configure_merge(env, options) do + Keyword.merge(env, options, fn + _, _v1, v2 -> v2 + end) + end + + defp log_event(level, msg, ts, md, state) do + output = format_event(level, msg, ts, md, state) |> IO.chardata_to_string() + ElixirLS.LanguageServer.JsonRpc.log_message(elixir_log_level_to_lsp(level), output) + state + end + + defp elixir_log_level_to_lsp(:debug), do: :log + defp elixir_log_level_to_lsp(:info), do: :info + defp elixir_log_level_to_lsp(:notice), do: :info + defp elixir_log_level_to_lsp(:warning), do: :warning + defp elixir_log_level_to_lsp(:warn), do: :warning + defp elixir_log_level_to_lsp(:error), do: :error + defp elixir_log_level_to_lsp(:critical), do: :error + defp elixir_log_level_to_lsp(:alert), do: :error + defp elixir_log_level_to_lsp(:emergency), do: :error + + defp format_event(level, msg, ts, md, state) do + %{format: format, metadata: keys} = state + + format + |> Logger.Formatter.format(level, msg, ts, take_metadata(md, keys)) + end + + defp take_metadata(metadata, :all) do + metadata + end + + defp take_metadata(metadata, keys) do + Enum.reduce(keys, [], fn key, acc -> + case Keyword.fetch(metadata, key) do + {:ok, val} -> [{key, val} | acc] + :error -> acc + end + end) + end +end diff --git a/apps/language_server/lib/language_server/providers/workspace_symbols.ex b/apps/language_server/lib/language_server/providers/workspace_symbols.ex index d2c399a66..ceec8a731 100644 --- a/apps/language_server/lib/language_server/providers/workspace_symbols.ex +++ b/apps/language_server/lib/language_server/providers/workspace_symbols.ex @@ -9,7 +9,7 @@ defmodule ElixirLS.LanguageServer.Providers.WorkspaceSymbols do alias ElixirLS.LanguageServer.ErlangSourceFile alias ElixirLS.LanguageServer.SourceFile alias ElixirLS.LanguageServer.Providers.SymbolUtils - alias ElixirLS.LanguageServer.JsonRpc + require Logger @arity_suffix_regex ~r/\/\d+$/ @@ -122,7 +122,7 @@ defmodule ElixirLS.LanguageServer.Providers.WorkspaceSymbols do callbacks_indexed: false } ) do - JsonRpc.log_message(:info, "[ElixirLS WorkspaceSymbols] Indexing...") + Logger.info("[ElixirLS WorkspaceSymbols] Indexing...") module_paths = :code.all_loaded() @@ -133,7 +133,7 @@ defmodule ElixirLS.LanguageServer.Providers.WorkspaceSymbols do do: {module, path} end) - JsonRpc.log_message(:info, "[ElixirLS WorkspaceSymbols] Module discovery complete") + Logger.info("[ElixirLS WorkspaceSymbols] Module discovery complete") index(module_paths) @@ -149,7 +149,7 @@ defmodule ElixirLS.LanguageServer.Providers.WorkspaceSymbols do modified_uris: modified_uris = [_ | _] } = state ) do - JsonRpc.log_message(:info, "[ElixirLS WorkspaceSymbols] Updating index...") + Logger.info("[ElixirLS WorkspaceSymbols] Updating index...") module_paths = :code.all_loaded() @@ -160,10 +160,7 @@ defmodule ElixirLS.LanguageServer.Providers.WorkspaceSymbols do do: {module, path} end) - JsonRpc.log_message( - :info, - "[ElixirLS WorkspaceSymbols] #{length(module_paths)} modules need reindexing" - ) + Logger.info("[ElixirLS WorkspaceSymbols] #{length(module_paths)} modules need reindexing") index(module_paths) @@ -428,10 +425,7 @@ defmodule ElixirLS.LanguageServer.Providers.WorkspaceSymbols do send(self, {:indexing_complete, key, results}) - JsonRpc.log_message( - :info, - "[ElixirLS WorkspaceSymbols] #{length(results)} #{key} added to index" - ) + Logger.info("[ElixirLS WorkspaceSymbols] #{length(results)} #{key} added to index") end) :ok diff --git a/apps/language_server/lib/language_server/server.ex b/apps/language_server/lib/language_server/server.ex index 1e82cb30a..b571a327c 100644 --- a/apps/language_server/lib/language_server/server.ex +++ b/apps/language_server/lib/language_server/server.ex @@ -16,6 +16,7 @@ defmodule ElixirLS.LanguageServer.Server do """ use GenServer + require Logger alias ElixirLS.LanguageServer.{SourceFile, Build, Protocol, JsonRpc, Dialyzer, Diagnostics} alias ElixirLS.LanguageServer.Providers.{ @@ -206,8 +207,7 @@ defmodule ElixirLS.LanguageServer.Server do state = case state do %{settings: nil} -> - JsonRpc.show_message( - :info, + Logger.warn( "Did not receive workspace/didChangeConfiguration notification after 5 seconds. " <> "Using default settings." ) @@ -279,10 +279,7 @@ defmodule ElixirLS.LanguageServer.Server do %{state | requests: Map.delete(requests, id)} _ -> - JsonRpc.log_message( - :warning, - "Received $/cancelRequest for unknown request id: #{inspect(id)}" - ) + Logger.warn("Received $/cancelRequest for unknown request id: #{inspect(id)}") state end @@ -322,8 +319,7 @@ defmodule ElixirLS.LanguageServer.Server do if Map.has_key?(state.source_files, uri) do # An open notification must not be sent more than once without a corresponding # close notification send before - JsonRpc.log_message( - :warning, + Logger.warn( "Received textDocument/didOpen for file that is already open. Received uri: #{inspect(uri)}" ) @@ -344,8 +340,7 @@ defmodule ElixirLS.LanguageServer.Server do defp handle_notification(did_close(uri), state = %__MODULE__{}) do if not Map.has_key?(state.source_files, uri) do # A close notification requires a previous open notification to be sent - JsonRpc.log_message( - :warning, + Logger.warn( "Received textDocument/didClose for file that is not open. Received uri: #{inspect(uri)}" ) @@ -366,8 +361,7 @@ defmodule ElixirLS.LanguageServer.Server do # The source file was not marked as open either due to a bug in the # client or a restart of the server. So just ignore the message and do # not update the state - JsonRpc.log_message( - :warning, + Logger.warn( "Received textDocument/didChange for file that is not open. Received uri: #{inspect(uri)}" ) @@ -382,8 +376,7 @@ defmodule ElixirLS.LanguageServer.Server do defp handle_notification(did_save(uri), state = %__MODULE__{}) do if not Map.has_key?(state.source_files, uri) do - JsonRpc.log_message( - :warning, + Logger.warn( "Received textDocument/didSave for file that is not open. Received uri: #{inspect(uri)}" ) @@ -445,7 +438,7 @@ defmodule ElixirLS.LanguageServer.Server do acc {:error, reason} -> - JsonRpc.log_message(:warning, "Unable to read #{uri}: #{inspect(reason)}") + Logger.warn("Unable to read #{uri}: #{inspect(reason)}") # keep dirty if read fails acc end @@ -473,7 +466,7 @@ defmodule ElixirLS.LanguageServer.Server do end defp handle_notification(packet, state = %__MODULE__{}) do - JsonRpc.log_message(:warning, "Received unmatched notification: #{inspect(packet)}") + Logger.warn("Received unmatched notification: #{inspect(packet)}") state end @@ -766,7 +759,7 @@ defmodule ElixirLS.LanguageServer.Server do fn -> case ExecuteCommand.execute(command, args, state) do {:error, :invalid_request, _msg} = res -> - JsonRpc.log_message(:warning, "Unmatched request: #{inspect(req)}") + Logger.warn("Unmatched request: #{inspect(req)}") res other -> @@ -796,7 +789,7 @@ defmodule ElixirLS.LanguageServer.Server do end defp handle_request(req, state = %__MODULE__{}) do - JsonRpc.log_message(:warning, "Unmatched request: #{inspect(req)}") + Logger.warn("Unmatched request: #{inspect(req)}") {:error, :invalid_request, nil, state} end @@ -1026,7 +1019,7 @@ defmodule ElixirLS.LanguageServer.Server do # If these results were triggered by the most recent build and files are not dirty, then we know # we're up to date and can release spec suggestions to the code lens provider if build_ref == state.build_ref do - JsonRpc.log_message(:info, "Dialyzer analysis is up to date") + Logger.info("Dialyzer analysis is up to date") {dirty, not_dirty} = state.awaiting_contracts @@ -1069,7 +1062,7 @@ defmodule ElixirLS.LanguageServer.Server do {:error, reason} -> if reason != :enoent do - IO.warn("Couldn't read file #{file}: #{inspect(reason)}") + Logger.warn("Couldn't read file #{file}: #{inspect(reason)}") end nil @@ -1101,7 +1094,7 @@ defmodule ElixirLS.LanguageServer.Server do case Dialyzer.check_support() do :ok -> :ok - {:error, msg} -> JsonRpc.show_message(:info, msg) + {:error, msg} -> JsonRpc.show_message(:warning, msg) end :ok @@ -1146,10 +1139,7 @@ defmodule ElixirLS.LanguageServer.Server do :ok other -> - JsonRpc.log_message( - :error, - "client/registerCapability returned: #{inspect(other)}" - ) + Logger.error("client/registerCapability returned: #{inspect(other)}") end state @@ -1180,8 +1170,11 @@ defmodule ElixirLS.LanguageServer.Server do else JsonRpc.show_message( :warning, - "You must restart ElixirLS after changing environment variables" + "Environment variables have changed. ElixirLS needs to restart" ) + + Process.sleep(5000) + System.halt(1) end state @@ -1193,7 +1186,10 @@ defmodule ElixirLS.LanguageServer.Server do if is_nil(prev_env) or env == prev_env do Mix.env(String.to_atom(env)) else - JsonRpc.show_message(:warning, "You must restart ElixirLS after changing Mix env") + JsonRpc.show_message(:warning, "Mix env change detected. ElixirLS will restart.") + + Process.sleep(5000) + System.halt(1) end state @@ -1213,7 +1209,10 @@ defmodule ElixirLS.LanguageServer.Server do if is_nil(prev_target) or target == prev_target do Mix.target(String.to_atom(target)) else - JsonRpc.show_message(:warning, "You must restart ElixirLS after changing Mix target") + JsonRpc.show_message(:warning, "Mix target change detected. ElixirLS will restart") + + Process.sleep(5000) + System.halt(1) end state @@ -1245,10 +1244,11 @@ defmodule ElixirLS.LanguageServer.Server do prev_project_dir != project_dir -> JsonRpc.show_message( :warning, - "You must restart ElixirLS after changing the project directory" + "Project directory change detected. ElixirLS will restart" ) - state + Process.sleep(5000) + System.halt(1) true -> state @@ -1271,10 +1271,7 @@ defmodule ElixirLS.LanguageServer.Server do state {:error, err} -> - JsonRpc.log_message( - :warning, - "Cannot create .elixir_ls/.gitignore, cause: #{Atom.to_string(err)}" - ) + Logger.warning("Cannot create .elixir_ls/.gitignore, cause: #{Atom.to_string(err)}") state end diff --git a/apps/language_server/lib/language_server/source_file.ex b/apps/language_server/lib/language_server/source_file.ex index ea3e1a591..e79feab77 100644 --- a/apps/language_server/lib/language_server/source_file.ex +++ b/apps/language_server/lib/language_server/source_file.ex @@ -1,5 +1,6 @@ defmodule ElixirLS.LanguageServer.SourceFile do import ElixirLS.LanguageServer.Protocol + require Logger defstruct [:text, :version, dirty?: false] @@ -248,7 +249,7 @@ defmodule ElixirLS.LanguageServer.SourceFile do e -> message = Exception.message(e) - IO.warn( + Logger.warn( "Unable to get formatter options for #{path}: #{inspect(e.__struct__)} #{message}" ) diff --git a/apps/language_server/test/providers/workspace_symbols_test.exs b/apps/language_server/test/providers/workspace_symbols_test.exs index 5699ba0c1..8ba943ac3 100644 --- a/apps/language_server/test/providers/workspace_symbols_test.exs +++ b/apps/language_server/test/providers/workspace_symbols_test.exs @@ -36,11 +36,6 @@ defmodule ElixirLS.LanguageServer.Providers.WorkspaceSymbolsTest do test "empty query", %{server: server} do assert {:ok, []} == WorkspaceSymbols.symbols("", server) - - assert_receive %{ - "method" => "window/logMessage", - "params" => %{"message" => "[ElixirLS WorkspaceSymbols] Updating index..."} - } end test "returns modules", %{server: server} do diff --git a/apps/language_server/test/source_file/invalid_project_test.exs b/apps/language_server/test/source_file/invalid_project_test.exs index a83076d06..dffec009c 100644 --- a/apps/language_server/test/source_file/invalid_project_test.exs +++ b/apps/language_server/test/source_file/invalid_project_test.exs @@ -3,7 +3,7 @@ defmodule ElixirLS.LanguageServer.SourceFile.InvalidProjectTest do use Patch alias ElixirLS.LanguageServer.SourceFile - import ExUnit.CaptureIO + import ExUnit.CaptureLog describe "formatter_for " do test "should handle syntax errors" do @@ -12,7 +12,7 @@ defmodule ElixirLS.LanguageServer.SourceFile.InvalidProjectTest do end) output = - capture_io(:stderr, fn -> + capture_log(fn -> assert :error = SourceFile.formatter_for("file:///root.ex") end) @@ -25,7 +25,7 @@ defmodule ElixirLS.LanguageServer.SourceFile.InvalidProjectTest do end) output = - capture_io(:stderr, fn -> + capture_log(fn -> assert :error = SourceFile.formatter_for("file:///root.ex") end) diff --git a/apps/language_server/test/support/server_test_helpers.ex b/apps/language_server/test/support/server_test_helpers.ex index 04a3a4f31..e36613055 100644 --- a/apps/language_server/test/support/server_test_helpers.ex +++ b/apps/language_server/test/support/server_test_helpers.ex @@ -9,6 +9,31 @@ defmodule ElixirLS.LanguageServer.Test.ServerTestHelpers do def start_server do packet_capture = start_supervised!({PacketCapture, self()}) + # :logger application is already started + # replace console logger with LSP + Application.put_env(:logger, :backends, [Logger.Backends.JsonRpc]) + + Application.put_env(:logger, Logger.Backends.JsonRpc, + level: :debug, + format: "$message", + metadata: [] + ) + + {:ok, _logger_backend} = Logger.add_backend(Logger.Backends.JsonRpc) + :ok = Logger.remove_backend(:console, flush: true) + + # Logger.add_backend returns Logger.Watcher pid + # the handler is supervised by :gen_event and the pid cannot be received via public api + # instead we call it to set group leader in the callback + :gen_event.call(Logger, Logger.Backends.JsonRpc, {:set_group_leader, packet_capture}) + + ExUnit.Callbacks.on_exit(fn -> + Application.put_env(:logger, :backends, [:console]) + + {:ok, _} = Logger.add_backend(:console) + :ok = Logger.remove_backend(Logger.Backends.JsonRpc, flush: false) + end) + server = start_supervised!({Server, nil}) Process.group_leader(server, packet_capture) From 45149aa04b34dc2acc02dcc6553ea51f3b6e52af Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Thu, 13 Oct 2022 21:10:36 +0200 Subject: [PATCH 076/125] improve debugger test stability --- .../elixir_ls_debugger/test/debugger_test.exs | 258 ++++++++++-------- 1 file changed, 145 insertions(+), 113 deletions(-) diff --git a/apps/elixir_ls_debugger/test/debugger_test.exs b/apps/elixir_ls_debugger/test/debugger_test.exs index 434cd3041..5c2a52d4b 100644 --- a/apps/elixir_ls_debugger/test/debugger_test.exs +++ b/apps/elixir_ls_debugger/test/debugger_test.exs @@ -29,64 +29,86 @@ defmodule ElixirLS.Debugger.ServerTest do describe "initialize" do test "succeeds", %{server: server} do - Server.receive_packet(server, initialize_req(1, %{"clientID" => "some_id"})) - assert_receive(response(_, 1, "initialize", %{"supportsConfigurationDoneRequest" => true})) - assert :sys.get_state(server).client_info == %{"clientID" => "some_id"} + in_fixture(__DIR__, "mix_project", fn -> + Server.receive_packet(server, initialize_req(1, %{"clientID" => "some_id"})) + + assert_receive( + response(_, 1, "initialize", %{"supportsConfigurationDoneRequest" => true}) + ) + + assert :sys.get_state(server).client_info == %{"clientID" => "some_id"} + end) end test "fails when already initialized", %{server: server} do - Server.receive_packet(server, initialize_req(1, %{"clientID" => "some_id"})) - assert_receive(response(_, 1, "initialize", %{"supportsConfigurationDoneRequest" => true})) - Server.receive_packet(server, initialize_req(2, %{"clientID" => "some_id"})) + in_fixture(__DIR__, "mix_project", fn -> + Server.receive_packet(server, initialize_req(1, %{"clientID" => "some_id"})) - assert_receive( - error_response( - _, - 2, - "initialize", - "invalidRequest", - "Debugger request {command} was not expected", - %{"command" => "initialize"} + assert_receive( + response(_, 1, "initialize", %{"supportsConfigurationDoneRequest" => true}) ) - ) + + Server.receive_packet(server, initialize_req(2, %{"clientID" => "some_id"})) + + assert_receive( + error_response( + _, + 2, + "initialize", + "invalidRequest", + "Debugger request {command} was not expected", + %{"command" => "initialize"} + ) + ) + end) end test "rejects requests when not initialized", %{server: server} do - Server.receive_packet( - server, - set_breakpoints_req(1, %{"path" => "lib/mix_project.ex"}, [%{"line" => 3}]) - ) + in_fixture(__DIR__, "mix_project", fn -> + Server.receive_packet( + server, + set_breakpoints_req(1, %{"path" => "lib/mix_project.ex"}, [%{"line" => 3}]) + ) - assert_receive( - error_response( - _, - 1, - "setBreakpoints", - "invalidRequest", - "Debugger request {command} was not expected", - %{"command" => "setBreakpoints"} + assert_receive( + error_response( + _, + 1, + "setBreakpoints", + "invalidRequest", + "Debugger request {command} was not expected", + %{"command" => "setBreakpoints"} + ) ) - ) + end) end end describe "disconnect" do test "succeeds when not initialized", %{server: server} do - Process.flag(:trap_exit, true) - Server.receive_packet(server, request(1, "disconnect")) - assert_receive(response(_, 1, "disconnect", %{})) - assert_receive({:EXIT, ^server, {:exit_code, 0}}) - Process.flag(:trap_exit, false) + in_fixture(__DIR__, "mix_project", fn -> + Process.flag(:trap_exit, true) + Server.receive_packet(server, request(1, "disconnect")) + assert_receive(response(_, 1, "disconnect", %{})) + assert_receive({:EXIT, ^server, {:exit_code, 0}}) + Process.flag(:trap_exit, false) + end) end test "succeeds when initialized", %{server: server} do - Process.flag(:trap_exit, true) - Server.receive_packet(server, initialize_req(1, %{"clientID" => "some_id"})) - assert_receive(response(_, 1, "initialize", %{"supportsConfigurationDoneRequest" => true})) - Server.receive_packet(server, request(2, "disconnect")) - assert_receive(response(_, 2, "disconnect", %{})) - assert_receive({:EXIT, ^server, {:exit_code, 0}}) - Process.flag(:trap_exit, false) + in_fixture(__DIR__, "mix_project", fn -> + Process.flag(:trap_exit, true) + Server.receive_packet(server, initialize_req(1, %{"clientID" => "some_id"})) + + assert_receive( + response(_, 1, "initialize", %{"supportsConfigurationDoneRequest" => true}) + ) + + Server.receive_packet(server, request(2, "disconnect")) + assert_receive(response(_, 2, "disconnect", %{})) + assert_receive({:EXIT, ^server, {:exit_code, 0}}) + Process.flag(:trap_exit, false) + end) end end @@ -1459,119 +1481,129 @@ defmodule ElixirLS.Debugger.ServerTest do end test "Evaluate expression with OK result", %{server: server} do - Server.receive_packet(server, initialize_req(1, %{})) - assert_receive(response(_, 1, "initialize", _)) + 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") - ) + Server.receive_packet( + server, + gen_watch_expression_packet("1 + 2 + 3 + 4") + ) - assert_receive(%{"body" => %{"result" => "10"}}, 1000) + assert_receive(%{"body" => %{"result" => "10"}}, 1000) - assert Process.alive?(server) + assert Process.alive?(server) + end) end @tag :capture_log test "Evaluate expression with ERROR result", %{server: server} do - Server.receive_packet(server, initialize_req(1, %{})) - assert_receive(response(_, 1, "initialize", _)) + 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") - ) + Server.receive_packet( + server, + gen_watch_expression_packet("1 = 2") + ) - assert_receive(%{"body" => %{"result" => result}}, 1000) + assert_receive(%{"body" => %{"result" => result}}, 1000) - assert result =~ ~r/badmatch/ + assert result =~ ~r/badmatch/ - assert Process.alive?(server) + assert Process.alive?(server) + end) end test "Evaluate expression with attempt to exit debugger process", %{server: server} do - Server.receive_packet(server, initialize_req(1, %{})) - assert_receive(response(_, 1, "initialize", _)) + 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)") - ) + Server.receive_packet( + server, + gen_watch_expression_packet("Process.exit(self(), :normal)") + ) - assert_receive(%{"body" => %{"result" => result}}, 1000) + assert_receive(%{"body" => %{"result" => result}}, 1000) - assert result =~ ~r/:exit/ + assert result =~ ~r/:exit/ - assert Process.alive?(server) + assert Process.alive?(server) + end) end test "Evaluate expression with attempt to throw debugger process", %{server: server} do - Server.receive_packet(server, initialize_req(1, %{})) - assert_receive(response(_, 1, "initialize", _)) + 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)") - ) + Server.receive_packet( + server, + gen_watch_expression_packet("throw(:goodmorning_bug)") + ) - assert_receive(%{"body" => %{"result" => result}}, 1000) + assert_receive(%{"body" => %{"result" => result}}, 1000) - assert result =~ ~r/:goodmorning_bug/ + assert result =~ ~r/:goodmorning_bug/ - assert Process.alive?(server) + assert Process.alive?(server) + end) end test "Evaluate expression which has long execution", %{server: server} do - Server.receive_packet(server, initialize_req(1, %{})) - assert_receive(response(_, 1, "initialize", _)) + in_fixture(__DIR__, "mix_project", fn -> + Server.receive_packet(server, initialize_req(1, %{})) + assert_receive(response(_, 1, "initialize", _)) - Server.receive_packet( - server, - launch_req(2, %{ - "request" => "launch", - "type" => "mix_task", - "task" => "test", - "projectDir" => File.cwd!(), - "debugExpressionTimeoutMs" => 500 - }) - ) + Server.receive_packet( + server, + launch_req(2, %{ + "request" => "launch", + "type" => "mix_task", + "task" => "test", + "projectDir" => File.cwd!(), + "debugExpressionTimeoutMs" => 500 + }) + ) - assert_receive(response(_, 2, "launch", %{}), 5000) + assert_receive(response(_, 2, "launch", %{}), 5000) - Server.receive_packet( - server, - gen_watch_expression_packet(":timer.sleep(10_000)") - ) + Server.receive_packet( + server, + gen_watch_expression_packet(":timer.sleep(10_000)") + ) - assert_receive(%{"body" => %{"result" => result}}, 1100) + assert_receive(%{"body" => %{"result" => result}}, 1100) - assert result =~ ~r/:elixir_ls_expression_timeout/ + assert result =~ ~r/:elixir_ls_expression_timeout/ - assert Process.alive?(server) + assert Process.alive?(server) + end) end end test "Completions", %{server: server} do - 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" - } - ) + in_fixture(__DIR__, "mix_project", fn -> + Server.receive_packet(server, initialize_req(1, %{})) + assert_receive(response(_, 1, "initialize", _)) - assert_receive(%{"body" => %{"targets" => _targets}}, 10000) + Server.receive_packet( + server, + %{ + "arguments" => %{ + "text" => "DateTi", + "column" => 7 + }, + "command" => "completions", + "seq" => 1, + "type" => "request" + } + ) - assert Process.alive?(server) + assert_receive(%{"body" => %{"targets" => _targets}}, 10000) - # assert [%{}] + assert Process.alive?(server) + end) end end From b55fece9c36489e025efa6367a4bcade8f04f737 Mon Sep 17 00:00:00 2001 From: Steve Cohen Date: Sat, 15 Oct 2022 04:47:47 -0700 Subject: [PATCH 077/125] Fixed unit tests that were broken before 1.13 (#749) The unit tests on builds prior to 1.13 were failing because I was patching the wrong function. Prior to 1.13, formatter_for_file didn't exist, but the unit tests would patch that but the `function_exported?` check in `source_file.ex` would skip that branch and succeed, which would cause the tests to fail. The change is simple, check to see if `formatter_for_file` exists, and if it doesn't, patch the `formatter_opts_for_file` function --- .../test/source_file/invalid_project_test.exs | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/apps/language_server/test/source_file/invalid_project_test.exs b/apps/language_server/test/source_file/invalid_project_test.exs index dffec009c..7ae7e46d7 100644 --- a/apps/language_server/test/source_file/invalid_project_test.exs +++ b/apps/language_server/test/source_file/invalid_project_test.exs @@ -5,9 +5,22 @@ defmodule ElixirLS.LanguageServer.SourceFile.InvalidProjectTest do alias ElixirLS.LanguageServer.SourceFile import ExUnit.CaptureLog + def appropriate_formatter_function_name(_) do + formatter_function = + if function_exported?(Mix.Tasks.Format, :formatter_for_file, 1) do + :formatter_for_file + else + :formatter_opts_for_file + end + + {:ok, formatter_name: formatter_function} + end + describe "formatter_for " do - test "should handle syntax errors" do - patch(Mix.Tasks.Format, :formatter_for_file, fn _ -> + setup [:appropriate_formatter_function_name] + + test "should handle syntax errors", ctx do + patch(Mix.Tasks.Format, ctx.formatter_name, fn _ -> raise %SyntaxError{file: ".formatter.exs", line: 1} end) @@ -19,8 +32,8 @@ defmodule ElixirLS.LanguageServer.SourceFile.InvalidProjectTest do assert String.contains?(output, "Unable to get formatter options") end - test "should handle compile errors" do - patch(Mix.Tasks.Format, :formatter_for_file, fn _ -> + test "should handle compile errors", ctx do + patch(Mix.Tasks.Format, ctx.formatter_name, fn _ -> raise %SyntaxError{file: ".formatter.exs", line: 1} end) From a4bd85e1983242e6767253036c81d85e1a55b885 Mon Sep 17 00:00:00 2001 From: Steve Cohen Date: Sat, 15 Oct 2022 05:00:48 -0700 Subject: [PATCH 078/125] Tracer was not being called via start_supervised (#750) * Tracer was not being called via start_supervised This caused it not to cleanly exit before the next test, but only sometimes. Flaky tests are no fun. * Changed a couple more start_link calls to start_supervised --- .../test/providers/execute_command/mix_clean_test.exs | 2 +- .../test/providers/workspace_symbols_test.exs | 3 +-- apps/language_server/test/server_test.exs | 8 ++++---- apps/language_server/test/tracer_test.exs | 2 +- 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/apps/language_server/test/providers/execute_command/mix_clean_test.exs b/apps/language_server/test/providers/execute_command/mix_clean_test.exs index e1c325944..cbda272f4 100644 --- a/apps/language_server/test/providers/execute_command/mix_clean_test.exs +++ b/apps/language_server/test/providers/execute_command/mix_clean_test.exs @@ -4,7 +4,7 @@ defmodule ElixirLS.LanguageServer.Providers.ExecuteCommand.MixCleanTest do use Protocol setup do - {:ok, _} = Tracer.start_link([]) + {:ok, _} = start_supervised(Tracer) server = ElixirLS.LanguageServer.Test.ServerTestHelpers.start_server() {:ok, %{server: server}} diff --git a/apps/language_server/test/providers/workspace_symbols_test.exs b/apps/language_server/test/providers/workspace_symbols_test.exs index 8ba943ac3..f850ac77c 100644 --- a/apps/language_server/test/providers/workspace_symbols_test.exs +++ b/apps/language_server/test/providers/workspace_symbols_test.exs @@ -5,8 +5,7 @@ defmodule ElixirLS.LanguageServer.Providers.WorkspaceSymbolsTest do setup do alias ElixirLS.Utils.PacketCapture packet_capture = start_supervised!({PacketCapture, self()}) - - {:ok, pid} = WorkspaceSymbols.start_link(name: nil) + {:ok, pid} = start_supervised({WorkspaceSymbols, name: nil}) Process.group_leader(pid, packet_capture) state = :sys.get_state(pid) diff --git a/apps/language_server/test/server_test.exs b/apps/language_server/test/server_test.exs index ec4ec933f..bb49655f7 100644 --- a/apps/language_server/test/server_test.exs +++ b/apps/language_server/test/server_test.exs @@ -235,13 +235,13 @@ defmodule ElixirLS.LanguageServer.ServerTest do end setup context do - unless context[:skip_server] do + if context[:skip_server] do + :ok + else server = ElixirLS.LanguageServer.Test.ServerTestHelpers.start_server() - {:ok, tracer} = Tracer.start_link([]) + {:ok, tracer} = start_supervised(Tracer) {:ok, %{server: server, tracer: tracer}} - else - :ok end end diff --git a/apps/language_server/test/tracer_test.exs b/apps/language_server/test/tracer_test.exs index 4840cfee9..1f2cf223b 100644 --- a/apps/language_server/test/tracer_test.exs +++ b/apps/language_server/test/tracer_test.exs @@ -6,7 +6,7 @@ defmodule ElixirLS.LanguageServer.TracerTest do setup context do File.rm_rf!(FixtureHelpers.get_path(".elixir_ls/calls.dets")) File.rm_rf!(FixtureHelpers.get_path(".elixir_ls/modules.dets")) - {:ok, _pid} = Tracer.start_link([]) + {:ok, _pid} = start_supervised(Tracer) {:ok, context} end From bdd50301d49a72dad5e89af29a1beb1c2a13b9f9 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Fri, 14 Oct 2022 21:52:11 +0200 Subject: [PATCH 079/125] use helper for mix file detection --- apps/language_server/lib/language_server/server.ex | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/language_server/lib/language_server/server.ex b/apps/language_server/lib/language_server/server.ex index b571a327c..510be390d 100644 --- a/apps/language_server/lib/language_server/server.ex +++ b/apps/language_server/lib/language_server/server.ex @@ -38,6 +38,7 @@ defmodule ElixirLS.LanguageServer.Server do alias ElixirLS.Utils.Launch alias ElixirLS.LanguageServer.Tracer + alias ElixirLS.Utils.MixfileHelpers use Protocol @@ -1239,7 +1240,7 @@ defmodule ElixirLS.LanguageServer.Server do is_nil(prev_project_dir) -> File.cd!(project_dir) - %{state | project_dir: File.cwd!(), mix_project?: File.exists?("mix.exs")} + %{state | project_dir: File.cwd!(), mix_project?: File.exists?(MixfileHelpers.mix_exs())} prev_project_dir != project_dir -> JsonRpc.show_message( From b6acb47bcf2eb032cc5fc55e1193015f9f760d2c Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Fri, 14 Oct 2022 22:14:39 +0200 Subject: [PATCH 080/125] rebuild project if tracing databases are not found --- apps/language_server/lib/language_server/server.ex | 9 +++++++++ apps/language_server/lib/language_server/tracer.ex | 4 ++++ 2 files changed, 13 insertions(+) diff --git a/apps/language_server/lib/language_server/server.ex b/apps/language_server/lib/language_server/server.ex index 510be390d..715e6f5d1 100644 --- a/apps/language_server/lib/language_server/server.ex +++ b/apps/language_server/lib/language_server/server.ex @@ -1120,6 +1120,7 @@ defmodule ElixirLS.LanguageServer.Server do |> set_dialyzer_enabled(enable_dialyzer) |> add_watched_extensions(additional_watched_extensions) + maybe_rebuild(state) state = create_gitignore(state) Tracer.set_project_dir(state.project_dir) trigger_build(%{state | settings: settings}) @@ -1298,4 +1299,12 @@ defmodule ElixirLS.LanguageServer.Server do _ -> false end) end + + defp maybe_rebuild(state = %__MODULE__{}) do + # detect if we are opening a project that has been compiled without a tracer + if is_binary(state.project_dir) and state.mix_project and File.dir?(Path.join([project_dir, ".elixir_ls"])) and not Tracer.has_databases?(project_dir) do + Logger.info("DETS databases rebuild will be rebuilt") + Build.clean(true) + end + end end diff --git a/apps/language_server/lib/language_server/tracer.ex b/apps/language_server/lib/language_server/tracer.ex index dc2d1d618..0255d1016 100644 --- a/apps/language_server/lib/language_server/tracer.ex +++ b/apps/language_server/lib/language_server/tracer.ex @@ -95,6 +95,10 @@ defmodule ElixirLS.LanguageServer.Tracer do :ok end + def has_databases?(project_dir) do + File.exists?(dets_path(project_dir, hd(@tables))) + end + defp dets_path(project_dir, table) do Path.join([project_dir, ".elixir_ls", "#{table}.dets"]) end From 6a52eb9bcb1f6f9f80f7813293f371162313ec27 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sun, 16 Oct 2022 16:43:34 +0200 Subject: [PATCH 081/125] write tracer manifast on succesful compile rebuild if manifest version does not match --- .../lib/language_server/build.ex | 2 +- .../lib/language_server/server.ex | 20 ++++-- .../lib/language_server/tracer.ex | 61 +++++++++++++++---- apps/language_server/test/tracer_test.exs | 15 +++++ 4 files changed, 81 insertions(+), 17 deletions(-) diff --git a/apps/language_server/lib/language_server/build.ex b/apps/language_server/lib/language_server/build.ex index eb3824c1e..7c84c10d5 100644 --- a/apps/language_server/lib/language_server/build.ex +++ b/apps/language_server/lib/language_server/build.ex @@ -58,7 +58,7 @@ defmodule ElixirLS.LanguageServer.Build do :global.trans({__MODULE__, self()}, func) end - defp reload_project do + def reload_project do mixfile = Path.absname(MixfileHelpers.mix_exs()) if File.exists?(mixfile) do diff --git a/apps/language_server/lib/language_server/server.ex b/apps/language_server/lib/language_server/server.ex index 715e6f5d1..a90abe50b 100644 --- a/apps/language_server/lib/language_server/server.ex +++ b/apps/language_server/lib/language_server/server.ex @@ -529,6 +529,7 @@ defmodule ElixirLS.LanguageServer.Server do "file://" <> _ -> root_path = SourceFile.Path.absolute_from_uri(root_uri) File.cd!(root_path) + Logger.info("cd initialize") cwd_uri = SourceFile.Path.to_uri(File.cwd!()) %{state | root_uri: cwd_uri} @@ -1241,6 +1242,7 @@ defmodule ElixirLS.LanguageServer.Server do is_nil(prev_project_dir) -> File.cd!(project_dir) + Logger.info("cd set_project_dir") %{state | project_dir: File.cwd!(), mix_project?: File.exists?(MixfileHelpers.mix_exs())} prev_project_dir != project_dir -> @@ -1300,11 +1302,21 @@ defmodule ElixirLS.LanguageServer.Server do end) end - defp maybe_rebuild(state = %__MODULE__{}) do + defp maybe_rebuild(state = %__MODULE__{project_dir: project_dir}) do # detect if we are opening a project that has been compiled without a tracer - if is_binary(state.project_dir) and state.mix_project and File.dir?(Path.join([project_dir, ".elixir_ls"])) and not Tracer.has_databases?(project_dir) do - Logger.info("DETS databases rebuild will be rebuilt") - Build.clean(true) + if is_binary(project_dir) and state.mix_project? and + File.dir?(Path.join([project_dir, ".elixir_ls"])) and + not Tracer.manifest_version_current?(project_dir) do + Logger.info("DETS databases will be rebuilt") + Tracer.clean_dets(project_dir) + + case Build.reload_project() do + {:ok, _} -> + Build.clean(true) + + _ -> + :ok + end end end end diff --git a/apps/language_server/lib/language_server/tracer.ex b/apps/language_server/lib/language_server/tracer.ex index 0255d1016..3b7d7023c 100644 --- a/apps/language_server/lib/language_server/tracer.ex +++ b/apps/language_server/lib/language_server/tracer.ex @@ -4,6 +4,8 @@ defmodule ElixirLS.LanguageServer.Tracer do use GenServer require Logger + @version 1 + @tables ~w(modules calls)a for table <- @tables do @@ -20,6 +22,10 @@ defmodule ElixirLS.LanguageServer.Tracer do GenServer.call(__MODULE__, {:set_project_dir, project_dir}) end + def save() do + GenServer.cast(__MODULE__, :save) + end + defp get_project_dir() do case Process.get(:elixir_ls_project_dir) do nil -> @@ -80,6 +86,19 @@ defmodule ElixirLS.LanguageServer.Tracer do {:reply, project_dir, state} end + @impl true + def handle_cast(:save, %{project_dir: project_dir} = state) do + for table <- @tables do + table_name = table_name(table) + + sync(table_name) + end + + write_manifest(project_dir) + + {:noreply, state} + end + @impl true def terminate(_reason, state) do maybe_close_tables(state) @@ -95,10 +114,6 @@ defmodule ElixirLS.LanguageServer.Tracer do :ok end - def has_databases?(project_dir) do - File.exists?(dets_path(project_dir, hd(@tables))) - end - defp dets_path(project_dir, table) do Path.join([project_dir, ".elixir_ls", "#{table}.dets"]) end @@ -270,14 +285,6 @@ defmodule ElixirLS.LanguageServer.Tracer do end) end - def save do - for table <- @tables do - table_name = table_name(table) - - sync(table_name) - end - end - defp sync(table_name) do with :ok <- :dets.from_ets(table_name, table_name), :ok <- :dets.sync(table_name) do @@ -336,4 +343,34 @@ defmodule ElixirLS.LanguageServer.Tracer do end end end + + defp manifest_path(project_dir) do + Path.join([project_dir, ".elixir_ls", "tracer_db.manifest"]) + end + + def write_manifest(project_dir) do + path = manifest_path(project_dir) + File.rm_rf!(path) + File.write!(path, "#{@version}") + end + + def read_manifest(project_dir) do + with {:ok, text} <- File.read(manifest_path(project_dir)), + {version, ""} <- Integer.parse(text) do + version + else + _ -> nil + end + end + + def manifest_version_current?(project_dir) do + read_manifest(project_dir) == @version + end + + def clean_dets(project_dir) do + for path <- + Path.join([project_dir, ".elixir_ls/*.dets"]) + |> Path.wildcard(), + do: File.rm_rf!(path) + end end diff --git a/apps/language_server/test/tracer_test.exs b/apps/language_server/test/tracer_test.exs index 1f2cf223b..67a7e46a2 100644 --- a/apps/language_server/test/tracer_test.exs +++ b/apps/language_server/test/tracer_test.exs @@ -4,6 +4,7 @@ defmodule ElixirLS.LanguageServer.TracerTest do alias ElixirLS.LanguageServer.Test.FixtureHelpers setup context do + File.rm_rf!(FixtureHelpers.get_path(".elixir_ls/tracer_db.manifest")) File.rm_rf!(FixtureHelpers.get_path(".elixir_ls/calls.dets")) File.rm_rf!(FixtureHelpers.get_path(".elixir_ls/modules.dets")) {:ok, _pid} = start_supervised(Tracer) @@ -206,4 +207,18 @@ defmodule ElixirLS.LanguageServer.TracerTest do assert [] == sorted_calls() end end + + describe "manifest" do + test "return nil when not found" do + project_path = FixtureHelpers.get_path("") + assert nil == Tracer.read_manifest(project_path) + end + + test "reads manifest" do + project_path = FixtureHelpers.get_path("") + Tracer.write_manifest(project_path) + + assert 1 == Tracer.read_manifest(project_path) + end + end end From 9fa72cfce1329535c3037d536f2c5859f08065fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Samson?= Date: Sat, 22 Oct 2022 20:10:19 +0200 Subject: [PATCH 082/125] Update mix case (#754) * do not start logger in debugger * Update mix case basing on elixir improvements this file was based on 5 years old implementation mix project stack etc clean has been changed since 1.6 elixir no longer sets :in_memory (https://github.com/elixir-lang/elixir/commit/73ab6b8eadfa4353f37d45be18a72bcb493ea48c) restoring mix stack turned out to be unnecessary plugged apps leakage (in debugger tests) --- apps/elixir_ls_debugger/mix.exs | 2 +- .../test/support/mix_test.case.ex | 96 +++++-------------- 2 files changed, 27 insertions(+), 71 deletions(-) diff --git a/apps/elixir_ls_debugger/mix.exs b/apps/elixir_ls_debugger/mix.exs index 83bfba583..88911380e 100644 --- a/apps/elixir_ls_debugger/mix.exs +++ b/apps/elixir_ls_debugger/mix.exs @@ -20,7 +20,7 @@ defmodule ElixirLS.Debugger.Mixfile do end def application do - [mod: {ElixirLS.Debugger, []}, extra_applications: [:mix, :logger]] + [mod: {ElixirLS.Debugger, []}, extra_applications: [:mix]] end defp deps do diff --git a/apps/elixir_ls_utils/test/support/mix_test.case.ex b/apps/elixir_ls_utils/test/support/mix_test.case.ex index 3226669e2..0ec49921e 100644 --- a/apps/elixir_ls_utils/test/support/mix_test.case.ex +++ b/apps/elixir_ls_utils/test/support/mix_test.case.ex @@ -1,7 +1,7 @@ defmodule ElixirLS.Utils.MixTest.Case do # This module is based heavily on MixTest.Case in Elixir's tests + # https://github.com/elixir-lang/elixir/blob/db64b413a036c01c8e1cac8dd5e1c65107d90176/lib/mix/test/test_helper.exs#L29 use ExUnit.CaseTemplate - alias ElixirLS.Utils.MixfileHelpers using do quote do @@ -9,32 +9,37 @@ defmodule ElixirLS.Utils.MixTest.Case do end end - setup config do - if apps = config[:apps] do - Logger.remove_backend(:console) - end + @apps Enum.map(Application.loaded_applications(), &elem(&1, 0)) + @allowed_apps ~w(docsh xmerl syntax_tools edoc elixir_sense elixir_ls_debugger elixir_ls_utils language_server stream_data)a + setup do on_exit(fn -> Application.start(:logger) + Mix.env(:dev) + Mix.target(:host) Mix.Task.clear() Mix.Shell.Process.flush() + Mix.State.clear_cache() + Mix.ProjectStack.clear_stack() delete_tmp_paths() - if apps do - for app <- apps do - Application.stop(app) - Application.unload(app) - end - - Logger.add_backend(:console, flush: true) + for {app, _, _} <- Application.loaded_applications(), + app not in @apps, + app not in @allowed_apps do + Application.stop(app) + Application.unload(app) end end) :ok end + def fixture_path(dir) do + Path.expand("fixtures", dir) + end + def fixture_path(dir, extension) do - Path.join(Path.expand("fixtures", dir), extension) + Path.join(fixture_path(dir), remove_colons(extension)) end def tmp_path do @@ -42,7 +47,13 @@ defmodule ElixirLS.Utils.MixTest.Case do end def tmp_path(extension) do - Path.join(tmp_path(), to_string(extension)) + Path.join(tmp_path(), remove_colons(extension)) + end + + defp remove_colons(term) do + term + |> to_string() + |> String.replace(":", "") end def purge(modules) do @@ -73,14 +84,6 @@ defmodule ElixirLS.Utils.MixTest.Case do get_path = :code.get_path() previous = :code.all_loaded() - project_stack = clear_project_stack!() - - ExUnit.CaptureLog.capture_log(fn -> - Application.stop(:mix) - Application.stop(:hex) - end) - - Application.start(:mix) try do File.cd!(dest, function) @@ -88,12 +91,10 @@ defmodule ElixirLS.Utils.MixTest.Case do :code.set_path(get_path) for {mod, file} <- :code.all_loaded() -- previous, - file == :in_memory or file == [] or (is_list(file) and :lists.prefix(flag, file)) do + file == [] or (is_list(file) and List.starts_with?(file, flag)) do mod end |> purge - - restore_project_stack!(project_stack) end end @@ -102,51 +103,6 @@ defmodule ElixirLS.Utils.MixTest.Case do for path <- :code.get_path(), :string.str(path, tmp) != 0, do: :code.del_path(path) end - defp clear_project_stack! do - stack = clear_project_stack!([]) - - # FIXME: Private API - Mix.State.clear_cache() - - # Attempt to purge mixfiles for dependencies to avoid module redefinition warnings - mix_exs = MixfileHelpers.mix_exs() - - for {mod, :in_memory} <- :code.all_loaded(), - source = mod.module_info[:compile][:source], - is_list(source), - String.ends_with?(to_string(source), mix_exs), - do: purge([mod]) - - stack - end - - defp clear_project_stack!(stack) do - # FIXME: Private API - case Mix.Project.pop() do - nil -> - stack - - project -> - clear_project_stack!([project | stack]) - end - end - - defp restore_project_stack!(stack) do - # FIXME: Private API - Mix.ProjectStack.clear_stack() - # FIXME: Private API - Mix.State.clear_cache() - - for %{name: module, file: file} <- stack do - :code.purge(module) - :code.delete(module) - # It's important to use `compile_file` here instead of `require_file` - # because we are recompiling this file to reload the mix project back onto - # the project stack. - Code.compile_file(file) - end - end - def capture_log_and_io(device, fun) when is_function(fun, 0) do # Logger gets stopped during some tests so restart it to be able to capture logs (and kept the # test output clean) From 0cbfed90f2933f41eec4eb515bc60ce2b7ca36cf Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sat, 22 Oct 2022 23:45:03 +0200 Subject: [PATCH 083/125] run static analysis on 1.14 --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 71a793b22..c4ffc6f5a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -90,8 +90,8 @@ jobs: strategy: matrix: include: - - elixir: 1.13.x - otp: 24.3.x + - elixir: 1.14.x + otp: 25.x steps: - uses: actions/checkout@v3 - uses: erlef/setup-beam@v1 From 2d59c20b8c17c465c48254eda592352f8bb338f5 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sat, 22 Oct 2022 23:45:32 +0200 Subject: [PATCH 084/125] actually run dialyzer and formatter --- .github/workflows/ci.yml | 25 +------------------------ 1 file changed, 1 insertion(+), 24 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c4ffc6f5a..2e124508e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,52 +19,33 @@ jobs: - elixir: 1.11.x otp: 22.3.x tests_may_fail: false - check_unused_deps: false - elixir: 1.11.x otp: 23.3.x tests_may_fail: false - check_unused_deps: false - elixir: 1.12.x otp: 23.3.x tests_may_fail: false - check_unused_deps: false - elixir: 1.13.x otp: 22.3.x tests_may_fail: false - check_unused_deps: true - warnings_as_errors: false - elixir: 1.13.x otp: 23.3.x tests_may_fail: false - check_unused_deps: false - warnings_as_errors: false - elixir: 1.13.x otp: 24.3.x tests_may_fail: false - check_unused_deps: false - warnings_as_errors: false - elixir: 1.13.x otp: 25.x tests_may_fail: false - check_unused_deps: false - warnings_as_errors: false - elixir: 1.14.x otp: 23.3.x tests_may_fail: false - check_unused_deps: false - warnings_as_errors: false - elixir: 1.14.x otp: 24.3.x tests_may_fail: false - check_unused_deps: false - warnings_as_errors: false - elixir: 1.14.x otp: 25.x tests_may_fail: false - warnings_as_errors: false - check_formatted: true - check_unused_deps: true - run_dialyzer: true env: MIX_ENV: test steps: @@ -78,10 +59,6 @@ jobs: mix local.hex --force mix local.rebar --force mix deps.get --only test - - run: mix format --check-formatted - if: matrix.check_formatted - - run: mix compile --warnings-as-errors - if: matrix.warnings_as_errors - run: mix test || ${{ matrix.tests_may_fail }} static_analysis: @@ -113,5 +90,5 @@ jobs: mix deps.get - name: Restore timestamps to prevent unnecessary recompilation run: IFS=$'\n'; for f in $(git ls-files); do touch -d "$(git log -n 1 --pretty='%cI' -- $f)" "$f"; done + - run: mix format --check-formatted - run: mix dialyzer - if: matrix.run_dialyzer From 4b980882781a90fa9d949f2325c39dd552c7cb98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Samson?= Date: Sat, 22 Oct 2022 23:46:00 +0200 Subject: [PATCH 085/125] Add custom command returning tests in file (#753) * add custom command returning tests in file * add tests * move outside of tests to prevent mix from running tests in fixture * disable other tracers * increase timeout * improve compatibility with elixir 1.11 and 1.12 * on_module is only available since 1.13 --- apps/language_server/.formatter.exs | 5 +- apps/language_server/lib/language_server.ex | 3 +- .../language_server/ex_unit_test_tracer.ex | 115 ++++++++++++++++++ .../providers/execute_command.ex | 3 +- .../get_ex_unit_tests_in_file.ex | 18 +++ .../lib/language_server/source_file.ex | 6 +- .../lib/language_server/tracer.ex | 18 ++- .../get_ex_unit_tests_in_file_test.exs | 55 +++++++++ apps/language_server/test/server_test.exs | 2 +- .../fixtures/project_with_tests/mix.exs | 9 ++ .../project_with_tests/test/error_test.exs | 21 ++++ .../project_with_tests/test/fixture_test.exs | 21 ++++ 12 files changed, 264 insertions(+), 12 deletions(-) create mode 100644 apps/language_server/lib/language_server/ex_unit_test_tracer.ex create mode 100644 apps/language_server/lib/language_server/providers/execute_command/get_ex_unit_tests_in_file.ex create mode 100644 apps/language_server/test/providers/execute_command/get_ex_unit_tests_in_file_test.exs create mode 100644 apps/language_server/test_fixtures/fixtures/project_with_tests/mix.exs create mode 100644 apps/language_server/test_fixtures/fixtures/project_with_tests/test/error_test.exs create mode 100644 apps/language_server/test_fixtures/fixtures/project_with_tests/test/fixture_test.exs diff --git a/apps/language_server/.formatter.exs b/apps/language_server/.formatter.exs index 4629fcb03..cfcc89ae3 100644 --- a/apps/language_server/.formatter.exs +++ b/apps/language_server/.formatter.exs @@ -1,4 +1,7 @@ -impossible_to_format = ["test/fixtures/token_missing_error/lib/has_error.ex"] +impossible_to_format = [ + "test/fixtures/token_missing_error/lib/has_error.ex", + "test/fixtures/project_with_tests/test/error_test.exs" +] [ inputs: diff --git a/apps/language_server/lib/language_server.ex b/apps/language_server/lib/language_server.ex index 36dfd96c1..0e7dc7f18 100644 --- a/apps/language_server/lib/language_server.ex +++ b/apps/language_server/lib/language_server.ex @@ -10,7 +10,8 @@ defmodule ElixirLS.LanguageServer do {ElixirLS.LanguageServer.Server, ElixirLS.LanguageServer.Server}, {ElixirLS.LanguageServer.JsonRpc, name: ElixirLS.LanguageServer.JsonRpc}, {ElixirLS.LanguageServer.Providers.WorkspaceSymbols, []}, - {ElixirLS.LanguageServer.Tracer, []} + {ElixirLS.LanguageServer.Tracer, []}, + {ElixirLS.LanguageServer.ExUnitTestTracer, []} ] opts = [strategy: :one_for_one, name: ElixirLS.LanguageServer.Supervisor, max_restarts: 0] diff --git a/apps/language_server/lib/language_server/ex_unit_test_tracer.ex b/apps/language_server/lib/language_server/ex_unit_test_tracer.ex new file mode 100644 index 000000000..9cfc75ec8 --- /dev/null +++ b/apps/language_server/lib/language_server/ex_unit_test_tracer.ex @@ -0,0 +1,115 @@ +defmodule ElixirLS.LanguageServer.ExUnitTestTracer do + use GenServer + + @tables ~w(tests)a + + for table <- @tables do + defp table_name(unquote(table)) do + :"#{__MODULE__}:#{unquote(table)}" + end + end + + def start_link(args) do + GenServer.start_link(__MODULE__, args, name: __MODULE__) + end + + def get_tests(path) do + GenServer.call(__MODULE__, {:get_tests, path}, :infinity) + end + + @impl true + def init(_args) do + for table <- @tables do + table_name = table_name(table) + + :ets.new(table_name, [ + :named_table, + :public, + read_concurrency: true, + write_concurrency: true + ]) + end + + ExUnit.start(autorun: false) + + {:ok, %{}} + end + + @impl true + def handle_call({:get_tests, path}, _from, state) do + :ets.delete_all_objects(table_name(:tests)) + tracers = Code.compiler_options()[:tracers] + # TODO build lock? + Code.put_compiler_option(:tracers, [__MODULE__]) + + result = + try do + # TODO parallel compiler and diagnostics? + _ = Code.compile_file(path) + + result = + :ets.tab2list(table_name(:tests)) + |> Enum.map(fn {{_file, module, line}, describes} -> + %{ + module: inspect(module), + line: line, + describes: describes + } + end) + + {:ok, result} + rescue + e -> + {:error, e} + after + Code.put_compiler_option(:tracers, tracers) + end + + {:reply, result, state} + end + + def trace({:on_module, _, _}, %Macro.Env{} = env) do + test_info = Module.get_attribute(env.module, :ex_unit_tests) + + if test_info != nil do + describe_infos = + test_info + |> Enum.group_by(fn %ExUnit.Test{tags: tags} -> {tags.describe, tags.describe_line} end) + |> Enum.map(fn {{describe, describe_line}, tests} -> + tests = + tests + |> Enum.map(fn %ExUnit.Test{tags: tags} = test -> + # drop test prefix + "test " <> test_name = Atom.to_string(test.name) + + test_name = + if describe != nil do + test_name |> String.replace_prefix(describe <> " ", "") + else + test_name + end + + %{ + name: test_name, + type: tags.test_type, + line: tags.line - 1 + } + end) + + %{ + describe: describe, + line: if(describe_line, do: describe_line - 1), + tests: tests + } + end) + + :ets.insert(table_name(:tests), {{env.file, env.module, env.line - 1}, describe_infos}) + end + + :ok + end + + def trace(_, %Macro.Env{} = _env) do + :ok + end +end diff --git a/apps/language_server/lib/language_server/providers/execute_command.ex b/apps/language_server/lib/language_server/providers/execute_command.ex index 427521244..414759acc 100644 --- a/apps/language_server/lib/language_server/providers/execute_command.ex +++ b/apps/language_server/lib/language_server/providers/execute_command.ex @@ -10,7 +10,8 @@ defmodule ElixirLS.LanguageServer.Providers.ExecuteCommand do "expandMacro" => ExecuteCommand.ExpandMacro, "manipulatePipes" => ExecuteCommand.ManipulatePipes, "restart" => ExecuteCommand.Restart, - "mixClean" => ExecuteCommand.MixClean + "mixClean" => ExecuteCommand.MixClean, + "getExUnitTestsInFile" => ExecuteCommand.GetExUnitTestsInFile } @callback execute([any], %ElixirLS.LanguageServer.Server{}) :: diff --git a/apps/language_server/lib/language_server/providers/execute_command/get_ex_unit_tests_in_file.ex b/apps/language_server/lib/language_server/providers/execute_command/get_ex_unit_tests_in_file.ex new file mode 100644 index 000000000..dfc167e7b --- /dev/null +++ b/apps/language_server/lib/language_server/providers/execute_command/get_ex_unit_tests_in_file.ex @@ -0,0 +1,18 @@ +defmodule ElixirLS.LanguageServer.Providers.ExecuteCommand.GetExUnitTestsInFile do + alias ElixirLS.LanguageServer.{SourceFile, ExUnitTestTracer} + @behaviour ElixirLS.LanguageServer.Providers.ExecuteCommand + + @impl ElixirLS.LanguageServer.Providers.ExecuteCommand + def execute([uri], _state) do + if Version.match?(System.version(), ">= 1.13.0") do + path = SourceFile.Path.from_uri(uri) + + case ExUnitTestTracer.get_tests(path) do + {:ok, tests} -> {:ok, tests} + {:error, reason} -> {:error, :server_error, inspect(reason)} + end + else + {:error, :server_error, "This feature requires elixir >= 1.13"} + end + end +end diff --git a/apps/language_server/lib/language_server/source_file.ex b/apps/language_server/lib/language_server/source_file.ex index e79feab77..ce7cec7e5 100644 --- a/apps/language_server/lib/language_server/source_file.ex +++ b/apps/language_server/lib/language_server/source_file.ex @@ -240,10 +240,10 @@ defmodule ElixirLS.LanguageServer.SourceFile do try do true = Code.ensure_loaded?(Mix.Tasks.Format) - if function_exported?(Mix.Tasks.Format, :formatter_for_file, 1) do - {:ok, Mix.Tasks.Format.formatter_for_file(path)} + if Version.match?(System.version(), ">= 1.13.0") do + {:ok, apply(Mix.Tasks.Format, :formatter_for_file, [path])} else - {:ok, {nil, Mix.Tasks.Format.formatter_opts_for_file(path)}} + {:ok, {nil, apply(Mix.Tasks.Format, :formatter_opts_for_file, [path])}} end rescue e -> diff --git a/apps/language_server/lib/language_server/tracer.ex b/apps/language_server/lib/language_server/tracer.ex index 3b7d7023c..ebdb8c156 100644 --- a/apps/language_server/lib/language_server/tracer.ex +++ b/apps/language_server/lib/language_server/tracer.ex @@ -206,14 +206,22 @@ defmodule ElixirLS.LanguageServer.Tracer do defp build_module_info(module, file, line) do defs = - for {name, arity} <- Module.definitions_in(module) do - def_info = Module.get_definition(module, {name, arity}) - {{name, arity}, build_def_info(def_info)} + if Version.match?(System.version(), ">= 1.12.0") do + for {name, arity} <- Module.definitions_in(module) do + def_info = apply(Module, :get_definition, [module, {name, arity}]) + {{name, arity}, build_def_info(def_info)} + end + else + [] end attributes = - for name <- Module.attributes_in(module) do - {name, Module.get_attribute(module, name)} + if Version.match?(System.version(), ">= 1.13.0") do + for name <- apply(Module, :attributes_in, [module]) do + {name, Module.get_attribute(module, name)} + end + else + [] end %{ diff --git a/apps/language_server/test/providers/execute_command/get_ex_unit_tests_in_file_test.exs b/apps/language_server/test/providers/execute_command/get_ex_unit_tests_in_file_test.exs new file mode 100644 index 000000000..688cc9f6a --- /dev/null +++ b/apps/language_server/test/providers/execute_command/get_ex_unit_tests_in_file_test.exs @@ -0,0 +1,55 @@ +defmodule ElixirLS.LanguageServer.Providers.ExecuteCommand.GetExUnitTestsInFileTest do + alias ElixirLS.LanguageServer.{ExUnitTestTracer, SourceFile} + alias ElixirLS.LanguageServer.Providers.ExecuteCommand.GetExUnitTestsInFile + use ElixirLS.Utils.MixTest.Case, async: false + + setup do + {:ok, _} = start_supervised(ExUnitTestTracer) + + {:ok, %{}} + end + + if Version.match?(System.version(), ">= 1.13.0") do + @tag fixture: true + test "return tests" do + in_fixture(Path.join(__DIR__, "../../../test_fixtures"), "project_with_tests", fn -> + uri = SourceFile.Path.to_uri(Path.join(File.cwd!(), "test/fixture_test.exs")) + + assert {:ok, + [ + %{ + describes: [ + %{ + describe: nil, + line: nil, + tests: [ + %{line: 19, name: "this will be a test in future", type: :test}, + %{line: 6, name: "fixture test", type: :test} + ] + }, + %{ + describe: "describe with test", + line: 10, + tests: [ + %{line: 11, name: "fixture test", type: :test} + ] + } + ], + line: 0, + module: "FixtureTest" + } + ]} = GetExUnitTestsInFile.execute([uri], nil) + end) + end + + @tag fixture: true + test "return error when file fails to compile" do + in_fixture(Path.join(__DIR__, "../../../test_fixtures"), "project_with_tests", fn -> + uri = SourceFile.Path.to_uri(Path.join(File.cwd!(), "test/error_test.exs")) + + assert {:error, :server_error, "%TokenMissingError" <> _} = + GetExUnitTestsInFile.execute([uri], nil) + end) + end + end +end diff --git a/apps/language_server/test/server_test.exs b/apps/language_server/test/server_test.exs index bb49655f7..ca8b3f453 100644 --- a/apps/language_server/test/server_test.exs +++ b/apps/language_server/test/server_test.exs @@ -726,7 +726,7 @@ defmodule ElixirLS.LanguageServer.ServerTest do Server.receive_packet(server, did_open(uri, "elixir", 1, code)) Server.receive_packet(server, completion_req(1, uri, 2, 25)) - resp = assert_receive(%{"id" => 1}, 1000) + resp = assert_receive(%{"id" => 1}, 5000) assert response(1, %{ "isIncomplete" => true, diff --git a/apps/language_server/test_fixtures/fixtures/project_with_tests/mix.exs b/apps/language_server/test_fixtures/fixtures/project_with_tests/mix.exs new file mode 100644 index 000000000..71a23daa8 --- /dev/null +++ b/apps/language_server/test_fixtures/fixtures/project_with_tests/mix.exs @@ -0,0 +1,9 @@ +defmodule ProjectWithTests.MixProject do + use Mix.Project + + def project do + [app: :project_with_tests, version: "0.1.0"] + end + + def application, do: [] +end diff --git a/apps/language_server/test_fixtures/fixtures/project_with_tests/test/error_test.exs b/apps/language_server/test_fixtures/fixtures/project_with_tests/test/error_test.exs new file mode 100644 index 000000000..2ef75b8c6 --- /dev/null +++ b/apps/language_server/test_fixtures/fixtures/project_with_tests/test/error_test.exs @@ -0,0 +1,21 @@ +defmodule ErrorTest do + use ExUnit.Case + + defmodule ModuleWithoutTests do + end + + test "fixture test" do + assert true + end + + describe "describe with test" do + test "fixture test" do + assert true + end + end + + describe "describe without test" do + end + + test "this will be a test in future" do +end diff --git a/apps/language_server/test_fixtures/fixtures/project_with_tests/test/fixture_test.exs b/apps/language_server/test_fixtures/fixtures/project_with_tests/test/fixture_test.exs new file mode 100644 index 000000000..dd215cb29 --- /dev/null +++ b/apps/language_server/test_fixtures/fixtures/project_with_tests/test/fixture_test.exs @@ -0,0 +1,21 @@ +defmodule FixtureTest do + use ExUnit.Case + + defmodule ModuleWithoutTests do + end + + test "fixture test" do + assert true + end + + describe "describe with test" do + test "fixture test" do + assert true + end + end + + describe "describe without test" do + end + + test "this will be a test in future" +end From 06259977458a48bd106402f6a6d6e1f222991337 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sat, 22 Oct 2022 23:54:25 +0200 Subject: [PATCH 086/125] race_conditions was removed in OTP 25 --- mix.exs | 1 - 1 file changed, 1 deletion(-) diff --git a/mix.exs b/mix.exs index a2654f81e..94b74be68 100644 --- a/mix.exs +++ b/mix.exs @@ -16,7 +16,6 @@ defmodule ElixirLS.Mixfile do # enable only to verify error handling # :unmatched_returns, :error_handling, - :race_conditions, :unknown, :underspecs ] From b9d76728eeca78c8f3b99a920e99f7950d095073 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sun, 30 Oct 2022 08:04:24 +0100 Subject: [PATCH 087/125] update elixir_sense --- mix.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.lock b/mix.lock index 4c0b07c5b..9287465ed 100644 --- a/mix.lock +++ b/mix.lock @@ -1,7 +1,7 @@ %{ "dialyxir": {:hex, :dialyxir, "1.1.0", "c5aab0d6e71e5522e77beff7ba9e08f8e02bad90dfbeffae60eaf0cb47e29488", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "07ea8e49c45f15264ebe6d5b93799d4dd56a44036cf42d0ad9c960bc266c0b9a"}, "docsh": {:hex, :docsh, "0.7.2", "f893d5317a0e14269dd7fe79cf95fb6b9ba23513da0480ec6e77c73221cae4f2", [:rebar3], [{:providers, "1.8.1", [hex: :providers, repo: "hexpm", optional: false]}], "hexpm", "4e7db461bb07540d2bc3d366b8513f0197712d0495bb85744f367d3815076134"}, - "elixir_sense": {:git, "https://github.com/elixir-lsp/elixir_sense.git", "12bdf7ac9971a9f1cb278b66364f912d63af4c0f", []}, + "elixir_sense": {:git, "https://github.com/elixir-lsp/elixir_sense.git", "8acc87c901f6fb89b0b18deb85ece6f527876964", []}, "erl2ex": {:git, "https://github.com/dazuma/erl2ex.git", "244c2d9ed5805ef4855a491d8616b8842fef7ca4", []}, "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, "getopt": {:hex, :getopt, "1.0.1", "c73a9fa687b217f2ff79f68a3b637711bb1936e712b521d8ce466b29cbf7808a", [:rebar3], [], "hexpm", "53e1ab83b9ceb65c9672d3e7a35b8092e9bdc9b3ee80721471a161c10c59959c"}, From a59ce3c3be38017e932093bbda87364aece2a0ea Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sun, 30 Oct 2022 09:13:30 +0100 Subject: [PATCH 088/125] update vendored jason to 1.4.0 --- mix.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.lock b/mix.lock index 9287465ed..f9b2fc325 100644 --- a/mix.lock +++ b/mix.lock @@ -5,7 +5,7 @@ "erl2ex": {:git, "https://github.com/dazuma/erl2ex.git", "244c2d9ed5805ef4855a491d8616b8842fef7ca4", []}, "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, "getopt": {:hex, :getopt, "1.0.1", "c73a9fa687b217f2ff79f68a3b637711bb1936e712b521d8ce466b29cbf7808a", [:rebar3], [], "hexpm", "53e1ab83b9ceb65c9672d3e7a35b8092e9bdc9b3ee80721471a161c10c59959c"}, - "jason_vendored": {:git, "https://github.com/elixir-lsp/jason.git", "ee95ca80cd67b3a499a14f469536140935eb4483", [branch: "vendored"]}, + "jason_vendored": {:git, "https://github.com/elixir-lsp/jason.git", "e23c65b98411a3066ca73534b4aed1d23bcf0356", [branch: "vendored"]}, "mix_task_archive_deps": {:git, "https://github.com/elixir-lsp/mix_task_archive_deps.git", "30fa76221def649286835685fec5d151be83c354", []}, "nimble_parsec": {:hex, :nimble_parsec, "1.1.0", "3a6fca1550363552e54c216debb6a9e95bd8d32348938e13de5eda962c0d7f89", [:mix], [], "hexpm", "08eb32d66b706e913ff748f11694b17981c0b04a33ef470e33e11b3d3ac8f54b"}, "patch": {:hex, :patch, "0.12.0", "2da8967d382bade20344a3e89d618bfba563b12d4ac93955468e830777f816b0", [:mix], [], "hexpm", "ffd0e9a7f2ad5054f37af84067ee88b1ad337308a1cb227e181e3967127b0235"}, From 84500aedc46ae872f8ad097758a3e90efe75c1fc Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sun, 30 Oct 2022 09:41:02 +0100 Subject: [PATCH 089/125] vendor dialyxir Fixes https://github.com/elixir-lsp/elixir-ls/issues/744 --- apps/elixir_ls_debugger/mix.exs | 2 +- apps/elixir_ls_utils/mix.exs | 2 +- apps/language_server/lib/language_server/dialyzer.ex | 2 +- apps/language_server/mix.exs | 2 +- mix.exs | 2 +- mix.lock | 1 + 6 files changed, 6 insertions(+), 5 deletions(-) diff --git a/apps/elixir_ls_debugger/mix.exs b/apps/elixir_ls_debugger/mix.exs index 88911380e..8cbf24f21 100644 --- a/apps/elixir_ls_debugger/mix.exs +++ b/apps/elixir_ls_debugger/mix.exs @@ -27,7 +27,7 @@ defmodule ElixirLS.Debugger.Mixfile do [ {:elixir_sense, github: "elixir-lsp/elixir_sense"}, {:elixir_ls_utils, in_umbrella: true}, - {:dialyxir, "~> 1.0", runtime: false} + {:dialyxir_vendored, github: "elixir-lsp/dialyxir", branch: "vendored", runtime: false} ] end end diff --git a/apps/elixir_ls_utils/mix.exs b/apps/elixir_ls_utils/mix.exs index b00cb2fa4..b2a02a397 100644 --- a/apps/elixir_ls_utils/mix.exs +++ b/apps/elixir_ls_utils/mix.exs @@ -29,7 +29,7 @@ defmodule ElixirLS.Utils.Mixfile do [ {:jason_vendored, github: "elixir-lsp/jason", branch: "vendored"}, {:mix_task_archive_deps, github: "elixir-lsp/mix_task_archive_deps"}, - {:dialyxir, "~> 1.0", runtime: false} + {:dialyxir_vendored, github: "elixir-lsp/dialyxir", branch: "vendored", runtime: false} ] end diff --git a/apps/language_server/lib/language_server/dialyzer.ex b/apps/language_server/lib/language_server/dialyzer.ex index 24feeb2ed..3b45e97d1 100644 --- a/apps/language_server/lib/language_server/dialyzer.ex +++ b/apps/language_server/lib/language_server/dialyzer.ex @@ -516,7 +516,7 @@ defmodule ElixirLS.LanguageServer.Dialyzer do end try do - %{^warning_name => warning_module} = Dialyxir.Warnings.warnings() + %{^warning_name => warning_module} = DialyxirVendored.Warnings.warnings() <<_::binary>> = apply(warning_module, format_function, [args]) rescue _ -> warning_message(raw_warning, "dialyzer") diff --git a/apps/language_server/mix.exs b/apps/language_server/mix.exs index 57dd80f2f..eb276bef3 100644 --- a/apps/language_server/mix.exs +++ b/apps/language_server/mix.exs @@ -29,7 +29,7 @@ defmodule ElixirLS.LanguageServer.Mixfile do {:elixir_ls_utils, in_umbrella: true}, {:elixir_sense, github: "elixir-lsp/elixir_sense"}, {:erl2ex, github: "dazuma/erl2ex"}, - {:dialyxir, "~> 1.0", runtime: false}, + {:dialyxir_vendored, github: "elixir-lsp/dialyxir", branch: "vendored", runtime: false}, {:jason_vendored, github: "elixir-lsp/jason", branch: "vendored"}, {:stream_data, "~> 0.5", only: :test}, {:path_glob_vendored, github: "elixir-lsp/path_glob", branch: "vendored"}, diff --git a/mix.exs b/mix.exs index 94b74be68..aa9899739 100644 --- a/mix.exs +++ b/mix.exs @@ -11,7 +11,7 @@ defmodule ElixirLS.Mixfile do deps: deps(), elixir: ">= 1.11.0", dialyzer: [ - plt_add_apps: [:dialyxir, :debugger, :dialyzer, :hipe], + plt_add_apps: [:dialyxir_vendored, :debugger, :dialyzer, :hipe], flags: [ # enable only to verify error handling # :unmatched_returns, diff --git a/mix.lock b/mix.lock index f9b2fc325..8eb4ecd4d 100644 --- a/mix.lock +++ b/mix.lock @@ -1,5 +1,6 @@ %{ "dialyxir": {:hex, :dialyxir, "1.1.0", "c5aab0d6e71e5522e77beff7ba9e08f8e02bad90dfbeffae60eaf0cb47e29488", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "07ea8e49c45f15264ebe6d5b93799d4dd56a44036cf42d0ad9c960bc266c0b9a"}, + "dialyxir_vendored": {:git, "https://github.com/elixir-lsp/dialyxir.git", "2f6f18f81f8dc4a5012b9810202e31737473874c", [branch: "vendored"]}, "docsh": {:hex, :docsh, "0.7.2", "f893d5317a0e14269dd7fe79cf95fb6b9ba23513da0480ec6e77c73221cae4f2", [:rebar3], [{:providers, "1.8.1", [hex: :providers, repo: "hexpm", optional: false]}], "hexpm", "4e7db461bb07540d2bc3d366b8513f0197712d0495bb85744f367d3815076134"}, "elixir_sense": {:git, "https://github.com/elixir-lsp/elixir_sense.git", "8acc87c901f6fb89b0b18deb85ece6f527876964", []}, "erl2ex": {:git, "https://github.com/dazuma/erl2ex.git", "244c2d9ed5805ef4855a491d8616b8842fef7ca4", []}, From e2e239ccce0031bfc7ad3055800d1d20115be11b Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sun, 30 Oct 2022 09:58:21 +0100 Subject: [PATCH 090/125] bump dialyxir --- mix.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.lock b/mix.lock index 8eb4ecd4d..30f777db5 100644 --- a/mix.lock +++ b/mix.lock @@ -1,6 +1,6 @@ %{ "dialyxir": {:hex, :dialyxir, "1.1.0", "c5aab0d6e71e5522e77beff7ba9e08f8e02bad90dfbeffae60eaf0cb47e29488", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "07ea8e49c45f15264ebe6d5b93799d4dd56a44036cf42d0ad9c960bc266c0b9a"}, - "dialyxir_vendored": {:git, "https://github.com/elixir-lsp/dialyxir.git", "2f6f18f81f8dc4a5012b9810202e31737473874c", [branch: "vendored"]}, + "dialyxir_vendored": {:git, "https://github.com/elixir-lsp/dialyxir.git", "b610d1a87885576e0e2aa4cfb5e176072bcd9457", [branch: "vendored"]}, "docsh": {:hex, :docsh, "0.7.2", "f893d5317a0e14269dd7fe79cf95fb6b9ba23513da0480ec6e77c73221cae4f2", [:rebar3], [{:providers, "1.8.1", [hex: :providers, repo: "hexpm", optional: false]}], "hexpm", "4e7db461bb07540d2bc3d366b8513f0197712d0495bb85744f367d3815076134"}, "elixir_sense": {:git, "https://github.com/elixir-lsp/elixir_sense.git", "8acc87c901f6fb89b0b18deb85ece6f527876964", []}, "erl2ex": {:git, "https://github.com/dazuma/erl2ex.git", "244c2d9ed5805ef4855a491d8616b8842fef7ca4", []}, From 91b5486e504a4c0bec6ff860005cfde0fe0ee906 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sun, 30 Oct 2022 09:58:44 +0100 Subject: [PATCH 091/125] fix task name in ci --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2e124508e..6fcff6508 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -91,4 +91,4 @@ jobs: - name: Restore timestamps to prevent unnecessary recompilation run: IFS=$'\n'; for f in $(git ls-files); do touch -d "$(git log -n 1 --pretty='%cI' -- $f)" "$f"; done - run: mix format --check-formatted - - run: mix dialyzer + - run: mix dialyzer_vendored From bab629a6c0371d6ad72795021f5166915363b4cc Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sun, 30 Oct 2022 09:59:05 +0100 Subject: [PATCH 092/125] hipe app was removed in OTP 25 --- mix.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.exs b/mix.exs index aa9899739..ffb5beb26 100644 --- a/mix.exs +++ b/mix.exs @@ -11,7 +11,7 @@ defmodule ElixirLS.Mixfile do deps: deps(), elixir: ">= 1.11.0", dialyzer: [ - plt_add_apps: [:dialyxir_vendored, :debugger, :dialyzer, :hipe], + plt_add_apps: [:dialyxir_vendored, :debugger, :dialyzer], flags: [ # enable only to verify error handling # :unmatched_returns, From 5c94b5d39534b59ca42e58435cf3ceab5618ae12 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sun, 30 Oct 2022 16:26:42 +0100 Subject: [PATCH 093/125] add ex_unit plt --- mix.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.exs b/mix.exs index ffb5beb26..1a61856ca 100644 --- a/mix.exs +++ b/mix.exs @@ -11,7 +11,7 @@ defmodule ElixirLS.Mixfile do deps: deps(), elixir: ">= 1.11.0", dialyzer: [ - plt_add_apps: [:dialyxir_vendored, :debugger, :dialyzer], + plt_add_apps: [:dialyxir_vendored, :debugger, :dialyzer, :ex_unit], flags: [ # enable only to verify error handling # :unmatched_returns, From f9725ef0b3afeaf6be2786e0eac9d885e09936ff Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sun, 30 Oct 2022 16:37:39 +0100 Subject: [PATCH 094/125] fix some dialyzer errors --- apps/elixir_ls_debugger/lib/debugger/breakpoint_condition.ex | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/elixir_ls_debugger/lib/debugger/breakpoint_condition.ex b/apps/elixir_ls_debugger/lib/debugger/breakpoint_condition.ex index 27c3639ca..6636a030c 100644 --- a/apps/elixir_ls_debugger/lib/debugger/breakpoint_condition.ex +++ b/apps/elixir_ls_debugger/lib/debugger/breakpoint_condition.ex @@ -39,7 +39,8 @@ defmodule ElixirLS.Debugger.BreakpointCondition do GenServer.call(name, {:has_condition?, {module, lines}}) end - @spec get_condition(module, non_neg_integer) :: {String.t(), non_neg_integer, non_neg_integer} + @spec get_condition(module, non_neg_integer) :: + {String.t(), String.t(), non_neg_integer, non_neg_integer} def get_condition(name \\ __MODULE__, number) do GenServer.call(name, {:get_condition, number}) end @@ -171,6 +172,7 @@ defmodule ElixirLS.Debugger.BreakpointCondition do end end + @spec eval_condition(String.t(), keyword) :: boolean def eval_condition("true", _binding), do: true def eval_condition(condition, elixir_binding) do From 5a7238832d673811306f03cb47fa5f01daf33a2b Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sun, 30 Oct 2022 16:48:17 +0100 Subject: [PATCH 095/125] fix dialyzer error --- .../lib/language_server/providers/formatting.ex | 11 +---------- .../lib/language_server/source_file.ex | 2 +- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/apps/language_server/lib/language_server/providers/formatting.ex b/apps/language_server/lib/language_server/providers/formatting.ex index 528956334..11d5b3f1e 100644 --- a/apps/language_server/lib/language_server/providers/formatting.ex +++ b/apps/language_server/lib/language_server/providers/formatting.ex @@ -14,13 +14,6 @@ defmodule ElixirLS.LanguageServer.Providers.Formatting do {:ok, []} end - {:ok, opts} -> - if should_format?(uri, project_dir, opts[:inputs]) do - do_format(source_file, opts) - else - {:ok, []} - end - :error -> {:error, :internal_error, "Unable to fetch formatter options"} end @@ -35,11 +28,9 @@ defmodule ElixirLS.LanguageServer.Providers.Formatting do # if project_dir is not set or schema is not file: we format with default options def format(%SourceFile{} = source_file, _uri, _project_dir) do - do_format(source_file) + do_format(source_file, nil, []) end - defp do_format(%SourceFile{} = source_file, opts \\ []), do: do_format(source_file, nil, opts) - defp do_format(%SourceFile{text: text}, formatter, opts) do formatted = get_formatted(text, formatter, opts) diff --git a/apps/language_server/lib/language_server/source_file.ex b/apps/language_server/lib/language_server/source_file.ex index ce7cec7e5..9cd549f13 100644 --- a/apps/language_server/lib/language_server/source_file.ex +++ b/apps/language_server/lib/language_server/source_file.ex @@ -233,7 +233,7 @@ defmodule ElixirLS.LanguageServer.SourceFile do """ end - @spec formatter_for(String.t()) :: {:ok, keyword()} | :error + @spec formatter_for(String.t()) :: {:ok, {function | nil, keyword()}} | :error def formatter_for(uri = "file:" <> _) do path = __MODULE__.Path.from_uri(uri) From 8d8eb4238ffea002509b758ceed4f94ef9fc10cf Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sun, 30 Oct 2022 16:50:44 +0100 Subject: [PATCH 096/125] enable more warnings --- .../lib/language_server/providers/workspace_symbols.ex | 2 +- mix.exs | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/language_server/lib/language_server/providers/workspace_symbols.ex b/apps/language_server/lib/language_server/providers/workspace_symbols.ex index ceec8a731..582a7165d 100644 --- a/apps/language_server/lib/language_server/providers/workspace_symbols.ex +++ b/apps/language_server/lib/language_server/providers/workspace_symbols.ex @@ -73,7 +73,7 @@ defmodule ElixirLS.LanguageServer.Providers.WorkspaceSymbols do end end - @spec notify_uris_modified([String.t()]) :: :ok + @spec notify_uris_modified([String.t()]) :: :ok | nil def notify_uris_modified(uris, server \\ __MODULE__, override_test_mode \\ false) do unless Application.get_env(:language_server, :test_mode) && not override_test_mode do GenServer.cast(server, {:uris_modified, uris}) diff --git a/mix.exs b/mix.exs index 1a61856ca..2417b94b9 100644 --- a/mix.exs +++ b/mix.exs @@ -17,7 +17,9 @@ defmodule ElixirLS.Mixfile do # :unmatched_returns, :error_handling, :unknown, - :underspecs + :underspecs, + :extra_return, + :missing_return ] ] ] From d3e05a6c3dfd01d4cdb993edc0cf1d41f77783cf Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sun, 30 Oct 2022 17:34:48 +0100 Subject: [PATCH 097/125] Warn if OTP built without doc chunks Fixes https://github.com/elixir-lsp/elixir-ls/issues/751 --- apps/language_server/lib/language_server/cli.ex | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/apps/language_server/lib/language_server/cli.ex b/apps/language_server/lib/language_server/cli.ex index 748e1b39d..f73bf0acc 100644 --- a/apps/language_server/lib/language_server/cli.ex +++ b/apps/language_server/lib/language_server/cli.ex @@ -38,6 +38,8 @@ defmodule ElixirLS.LanguageServer.CLI do "Running on elixir #{versions.current_elixir_version} on OTP #{versions.current_otp_version}" ) + check_otp_doc_chunks() + Launch.limit_num_schedulers() Mix.shell(ElixirLS.LanguageServer.MixShell) @@ -82,4 +84,11 @@ defmodule ElixirLS.LanguageServer.CLI do raise message end end + + def check_otp_doc_chunks() do + if match?({:error, _}, Code.fetch_docs(:erlang)) do + JsonRpc.show_message(:warning, "OTP compiled without EEP48 documentation chunks") + Logger.warn("OTP compiled without EEP48 documentation chunks. Language features for erlang modules will run in limited mode. Please reinstall or rebuild OTP with approperiate flags.") + end + end end From 52dae7a5515eaf4c03ec768cfba8d6f17d147aa5 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sun, 30 Oct 2022 21:56:58 +0100 Subject: [PATCH 098/125] remove not needed logs --- apps/language_server/lib/language_server/server.ex | 2 -- 1 file changed, 2 deletions(-) diff --git a/apps/language_server/lib/language_server/server.ex b/apps/language_server/lib/language_server/server.ex index a90abe50b..b8ea11f5d 100644 --- a/apps/language_server/lib/language_server/server.ex +++ b/apps/language_server/lib/language_server/server.ex @@ -529,7 +529,6 @@ defmodule ElixirLS.LanguageServer.Server do "file://" <> _ -> root_path = SourceFile.Path.absolute_from_uri(root_uri) File.cd!(root_path) - Logger.info("cd initialize") cwd_uri = SourceFile.Path.to_uri(File.cwd!()) %{state | root_uri: cwd_uri} @@ -1242,7 +1241,6 @@ defmodule ElixirLS.LanguageServer.Server do is_nil(prev_project_dir) -> File.cd!(project_dir) - Logger.info("cd set_project_dir") %{state | project_dir: File.cwd!(), mix_project?: File.exists?(MixfileHelpers.mix_exs())} prev_project_dir != project_dir -> From 32e4c2959d8b6ae99d0319aa13be940f3cf1cc15 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sun, 30 Oct 2022 21:58:36 +0100 Subject: [PATCH 099/125] Warn if unable to locate elixir and OTP sources Fixes https://github.com/elixir-lsp/elixir_sense/issues/44 --- .../lib/language_server/cli.ex | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/apps/language_server/lib/language_server/cli.ex b/apps/language_server/lib/language_server/cli.ex index f73bf0acc..0e1280e4f 100644 --- a/apps/language_server/lib/language_server/cli.ex +++ b/apps/language_server/lib/language_server/cli.ex @@ -39,6 +39,8 @@ defmodule ElixirLS.LanguageServer.CLI do ) check_otp_doc_chunks() + check_elixir_sources() + check_otp_sources() Launch.limit_num_schedulers() @@ -91,4 +93,21 @@ defmodule ElixirLS.LanguageServer.CLI do Logger.warn("OTP compiled without EEP48 documentation chunks. Language features for erlang modules will run in limited mode. Please reinstall or rebuild OTP with approperiate flags.") end end + + def check_elixir_sources() do + enum_ex_path = Enum.module_info[:compile][:source] + unless File.exists?(enum_ex_path, [:raw]) do + dir = Path.join(enum_ex_path, "../../../..") |> Path.expand + Logger.notice("Elixir sources not found (checking in #{dir}). Code navigation to Elixir modules disabled.") + end + end + + def check_otp_sources() do + {_module, _binary, beam_filename} = :code.get_object_code(:erlang) + erlang_erl_path = beam_filename |> to_string |> String.replace(Regex.recompile!(~r/(.+)\/ebin\/([^\s]+)\.beam$/), "\\1/src/\\2.erl") + unless File.exists?(erlang_erl_path, [:raw]) do + dir = Path.join(erlang_erl_path, "../../../..") |> Path.expand + Logger.notice("OTP sources not found (checking in #{dir}). Code navigation to OTP modules disabled.") + end + end end From 685c2fa6ef2b9d1702b34f372a6e479a01646af9 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Mon, 31 Oct 2022 07:49:15 +0100 Subject: [PATCH 100/125] Display message and help log if unable to build PLTs Disable dialyzer and continue Fixes https://github.com/elixir-lsp/elixir-ls/issues/540 --- .../lib/language_server/dialyzer/manifest.ex | 23 +++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/apps/language_server/lib/language_server/dialyzer/manifest.ex b/apps/language_server/lib/language_server/dialyzer/manifest.ex index 69f56a042..abf336f1f 100644 --- a/apps/language_server/lib/language_server/dialyzer/manifest.ex +++ b/apps/language_server/lib/language_server/dialyzer/manifest.ex @@ -12,10 +12,25 @@ defmodule ElixirLS.LanguageServer.Dialyzer.Manifest do parent = self() Task.start_link(fn -> - active_plt = load_elixir_plt() - transfer_plt(active_plt, parent) - - Dialyzer.analysis_finished(parent, :noop, active_plt, %{}, %{}, %{}, nil, nil) + watcher = self() + + {pid, ref} = spawn_monitor(fn -> + active_plt = load_elixir_plt() + send(watcher, :plt_loaded) + transfer_plt(active_plt, parent) + + Dialyzer.analysis_finished(parent, :noop, active_plt, %{}, %{}, %{}, nil, nil) + end) + + receive do + :plt_loaded -> :ok + {:DOWN, ^ref, :process, ^pid, reason} -> + JsonRpc.show_message(:error, "Unable to build dialyzer PLT. Most likely there are problems with your OTP and elixir installation.") + Logger.error("Dialyzer PLT build process exited with reason: #{inspect(reason)}") + Logger.warn("Dialyzer support disabled. Most likely there are problems with your elixir and OTP installation. Visit https://github.com/elixir-lsp/elixir-ls/issues/540 for help") + # NOTE We do not call Dialyzer.analysis_finished. LS keeps working and building normally + # only dialyzer is not being triggered after every build + end end) end From e996a387620bd3b861e23e5fadfd49ffcb2471fa Mon Sep 17 00:00:00 2001 From: Ajay <59688988+ajayvigneshk@users.noreply.github.com> Date: Tue, 1 Nov 2022 13:43:09 +0530 Subject: [PATCH 101/125] Add alias as additionalTextEdits (#722) --- .../language_server/providers/completion.ex | 65 ++++++++++++++++++- .../test/providers/completion_test.exs | 42 ++++++++++++ 2 files changed, 104 insertions(+), 3 deletions(-) diff --git a/apps/language_server/lib/language_server/providers/completion.ex b/apps/language_server/lib/language_server/providers/completion.ex index 295177070..3fdb672a0 100644 --- a/apps/language_server/lib/language_server/providers/completion.ex +++ b/apps/language_server/lib/language_server/providers/completion.ex @@ -7,7 +7,9 @@ defmodule ElixirLS.LanguageServer.Providers.Completion do with the Language Server Protocol. We also attempt to determine the context based on the line text before the cursor so we can filter out suggestions that are not relevant. """ + alias ElixirLS.LanguageServer.Protocol.TextEdit alias ElixirLS.LanguageServer.SourceFile + import ElixirLS.LanguageServer.Protocol, only: [range: 4] @enforce_keys [:label, :kind, :insert_text, :priority, :tags] defstruct [ @@ -21,7 +23,8 @@ defmodule ElixirLS.LanguageServer.Providers.Completion do :priority, :tags, :command, - {:preselect, false} + {:preselect, false}, + :additional_text_edit ] @func_snippets %{ @@ -102,8 +105,10 @@ defmodule ElixirLS.LanguageServer.Providers.Completion do # TODO: Don't call into here directly # Can we use ElixirSense.Providers.Suggestion? ElixirSense.suggestions/3 + metadata = ElixirSense.Core.Parser.parse_string(text, true, true, line) + env = - ElixirSense.Core.Parser.parse_string(text, true, true, line) + metadata |> ElixirSense.Core.Metadata.get_env(line) scope = @@ -139,8 +144,18 @@ defmodule ElixirLS.LanguageServer.Providers.Completion do module: env.module } + position_to_insert_alias = + ElixirSense.Core.Metadata.get_position_to_insert_alias(metadata, line) || {line, 0} + + context = + Map.put( + context, + :position_to_insert_alias, + SourceFile.elixir_position_to_lsp(text, position_to_insert_alias) + ) + items = - ElixirSense.suggestions(text, line, character) + ElixirSense.suggestions(text, line, character, required_alias: true) |> maybe_reject_derived_functions(context, options) |> Enum.map(&from_completion_item(&1, context, options)) |> maybe_add_do(context) @@ -285,6 +300,44 @@ defmodule ElixirLS.LanguageServer.Providers.Completion do } end + defp from_completion_item( + %{ + type: :module, + name: name, + summary: summary, + subtype: subtype, + metadata: metadata, + required_alias: required_alias + }, + %{ + def_before: nil, + position_to_insert_alias: {line_to_insert_alias, column_to_insert_alias} + }, + options + ) do + completion_without_additional_text_edit = + from_completion_item( + %{type: :module, name: name, summary: summary, subtype: subtype, metadata: metadata}, + %{def_before: nil}, + options + ) + + alias_value = + Atom.to_string(required_alias) + |> String.replace_prefix("Elixir.", "") + + indentation = 1..column_to_insert_alias//1 |> Enum.map(fn _ -> " " end) |> Enum.join() + alias_edit = indentation <> "alias " <> alias_value <> "\n" + + struct(completion_without_additional_text_edit, + additional_text_edit: %TextEdit{ + range: range(line_to_insert_alias, 0, line_to_insert_alias, 0), + newText: alias_edit + }, + documentation: alias_value <> "\n" <> summary + ) + end + defp from_completion_item( %{type: :module, name: name, summary: summary, subtype: subtype, metadata: metadata}, %{def_before: nil}, @@ -989,6 +1042,12 @@ defmodule ElixirLS.LanguageServer.Providers.Completion do "filterText" => item.filter_text, "sortText" => String.pad_leading(to_string(idx), 8, "0"), "insertText" => item.insert_text, + "additionalTextEdits" => + if item.additional_text_edit do + [item.additional_text_edit] + else + nil + end, "command" => item.command, "insertTextFormat" => if Keyword.get(options, :snippets_supported, false) do diff --git a/apps/language_server/test/providers/completion_test.exs b/apps/language_server/test/providers/completion_test.exs index 941b32242..41d2a7c6b 100644 --- a/apps/language_server/test/providers/completion_test.exs +++ b/apps/language_server/test/providers/completion_test.exs @@ -368,6 +368,48 @@ defmodule ElixirLS.LanguageServer.Providers.CompletionTest do end describe "structs and maps" do + test "suggests full module path as additionalTextEdits" do + text = """ + defmodule MyModule do + @moduledoc \"\"\" + This + is a + long + moduledoc + + \"\"\" + + def dummy_function() do + ExampleS + # ^ + end + end + """ + + {line, char} = {10, 12} + TestUtils.assert_has_cursor_char(text, line, char) + + {:ok, %{"items" => items}} = Completion.completion(text, line, char, @supports) + + assert [item] = items + + # 22 is struct + assert item["kind"] == 22 + assert item["label"] == "ExampleStruct (struct)" + + assert [%{newText: "alias ElixirLS.LanguageServer.Fixtures.ExampleStruct\n"}] = + item["additionalTextEdits"] + + assert [ + %{ + range: %{ + "end" => %{"character" => 0, "line" => 8}, + "start" => %{"character" => 0, "line" => 8} + } + } + ] = item["additionalTextEdits"] + end + test "completions of structs are rendered as a struct" do text = """ defmodule MyModule do From 9e49ac191d4ecafca0a5acb36ada7082d94f571e Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Tue, 1 Nov 2022 09:26:08 +0100 Subject: [PATCH 102/125] fix build on elixir 1.11 --- .../language_server/lib/language_server/providers/completion.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/language_server/lib/language_server/providers/completion.ex b/apps/language_server/lib/language_server/providers/completion.ex index 3fdb672a0..3c7dd7f66 100644 --- a/apps/language_server/lib/language_server/providers/completion.ex +++ b/apps/language_server/lib/language_server/providers/completion.ex @@ -326,7 +326,7 @@ defmodule ElixirLS.LanguageServer.Providers.Completion do Atom.to_string(required_alias) |> String.replace_prefix("Elixir.", "") - indentation = 1..column_to_insert_alias//1 |> Enum.map(fn _ -> " " end) |> Enum.join() + indentation = 1..column_to_insert_alias |> Enum.map(fn _ -> " " end) |> Enum.join() alias_edit = indentation <> "alias " <> alias_value <> "\n" struct(completion_without_additional_text_edit, From 699a728b0362b2a10a511a29dba716e7fa0b6cfd Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Tue, 1 Nov 2022 10:12:35 +0100 Subject: [PATCH 103/125] fix build on elixir 1.11 --- .../lib/language_server/providers/completion.ex | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/language_server/lib/language_server/providers/completion.ex b/apps/language_server/lib/language_server/providers/completion.ex index 3c7dd7f66..d968270c0 100644 --- a/apps/language_server/lib/language_server/providers/completion.ex +++ b/apps/language_server/lib/language_server/providers/completion.ex @@ -326,7 +326,11 @@ defmodule ElixirLS.LanguageServer.Providers.Completion do Atom.to_string(required_alias) |> String.replace_prefix("Elixir.", "") - indentation = 1..column_to_insert_alias |> Enum.map(fn _ -> " " end) |> Enum.join() + indentation = + if column_to_insert_alias >= 1, + do: 1..column_to_insert_alias |> Enum.map_join(fn _ -> " " end), + else: "" + alias_edit = indentation <> "alias " <> alias_value <> "\n" struct(completion_without_additional_text_edit, From 75ec61ef2e2bdaf3f9cd4af061e23d3c7dcba644 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Tue, 1 Nov 2022 10:30:22 +0100 Subject: [PATCH 104/125] run formatter --- .../lib/language_server/cli.ex | 29 ++++++++++++++----- .../lib/language_server/dialyzer/manifest.ex | 29 +++++++++++++------ 2 files changed, 42 insertions(+), 16 deletions(-) diff --git a/apps/language_server/lib/language_server/cli.ex b/apps/language_server/lib/language_server/cli.ex index 0e1280e4f..b5b0c12f8 100644 --- a/apps/language_server/lib/language_server/cli.ex +++ b/apps/language_server/lib/language_server/cli.ex @@ -90,24 +90,39 @@ defmodule ElixirLS.LanguageServer.CLI do def check_otp_doc_chunks() do if match?({:error, _}, Code.fetch_docs(:erlang)) do JsonRpc.show_message(:warning, "OTP compiled without EEP48 documentation chunks") - Logger.warn("OTP compiled without EEP48 documentation chunks. Language features for erlang modules will run in limited mode. Please reinstall or rebuild OTP with approperiate flags.") + + Logger.warn( + "OTP compiled without EEP48 documentation chunks. Language features for erlang modules will run in limited mode. Please reinstall or rebuild OTP with approperiate flags." + ) end end def check_elixir_sources() do - enum_ex_path = Enum.module_info[:compile][:source] + enum_ex_path = Enum.module_info()[:compile][:source] + unless File.exists?(enum_ex_path, [:raw]) do - dir = Path.join(enum_ex_path, "../../../..") |> Path.expand - Logger.notice("Elixir sources not found (checking in #{dir}). Code navigation to Elixir modules disabled.") + dir = Path.join(enum_ex_path, "../../../..") |> Path.expand() + + Logger.notice( + "Elixir sources not found (checking in #{dir}). Code navigation to Elixir modules disabled." + ) end end def check_otp_sources() do {_module, _binary, beam_filename} = :code.get_object_code(:erlang) - erlang_erl_path = beam_filename |> to_string |> String.replace(Regex.recompile!(~r/(.+)\/ebin\/([^\s]+)\.beam$/), "\\1/src/\\2.erl") + + erlang_erl_path = + beam_filename + |> to_string + |> String.replace(Regex.recompile!(~r/(.+)\/ebin\/([^\s]+)\.beam$/), "\\1/src/\\2.erl") + unless File.exists?(erlang_erl_path, [:raw]) do - dir = Path.join(erlang_erl_path, "../../../..") |> Path.expand - Logger.notice("OTP sources not found (checking in #{dir}). Code navigation to OTP modules disabled.") + dir = Path.join(erlang_erl_path, "../../../..") |> Path.expand() + + Logger.notice( + "OTP sources not found (checking in #{dir}). Code navigation to OTP modules disabled." + ) end end end diff --git a/apps/language_server/lib/language_server/dialyzer/manifest.ex b/apps/language_server/lib/language_server/dialyzer/manifest.ex index abf336f1f..bdf09d1a0 100644 --- a/apps/language_server/lib/language_server/dialyzer/manifest.ex +++ b/apps/language_server/lib/language_server/dialyzer/manifest.ex @@ -14,20 +14,31 @@ defmodule ElixirLS.LanguageServer.Dialyzer.Manifest do Task.start_link(fn -> watcher = self() - {pid, ref} = spawn_monitor(fn -> - active_plt = load_elixir_plt() - send(watcher, :plt_loaded) - transfer_plt(active_plt, parent) + {pid, ref} = + spawn_monitor(fn -> + active_plt = load_elixir_plt() + send(watcher, :plt_loaded) + transfer_plt(active_plt, parent) - Dialyzer.analysis_finished(parent, :noop, active_plt, %{}, %{}, %{}, nil, nil) - end) + Dialyzer.analysis_finished(parent, :noop, active_plt, %{}, %{}, %{}, nil, nil) + end) receive do - :plt_loaded -> :ok + :plt_loaded -> + :ok + {:DOWN, ^ref, :process, ^pid, reason} -> - JsonRpc.show_message(:error, "Unable to build dialyzer PLT. Most likely there are problems with your OTP and elixir installation.") + JsonRpc.show_message( + :error, + "Unable to build dialyzer PLT. Most likely there are problems with your OTP and elixir installation." + ) + Logger.error("Dialyzer PLT build process exited with reason: #{inspect(reason)}") - Logger.warn("Dialyzer support disabled. Most likely there are problems with your elixir and OTP installation. Visit https://github.com/elixir-lsp/elixir-ls/issues/540 for help") + + Logger.warn( + "Dialyzer support disabled. Most likely there are problems with your elixir and OTP installation. Visit https://github.com/elixir-lsp/elixir-ls/issues/540 for help" + ) + # NOTE We do not call Dialyzer.analysis_finished. LS keeps working and building normally # only dialyzer is not being triggered after every build end From 72b432459c61d8a5f1f4c7133d2cba5ef7aaa7d5 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Tue, 1 Nov 2022 10:33:07 +0100 Subject: [PATCH 105/125] run formatter in language_server app Path.wildcard is not working on umbrella level --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6fcff6508..46338b99c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -91,4 +91,5 @@ jobs: - name: Restore timestamps to prevent unnecessary recompilation run: IFS=$'\n'; for f in $(git ls-files); do touch -d "$(git log -n 1 --pretty='%cI' -- $f)" "$f"; done - run: mix format --check-formatted + - run: cd apps/language_server && mix format --check-formatted - run: mix dialyzer_vendored From 2dd1e00a9f4f5be07da021f7fcadc2983358c09d Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Tue, 1 Nov 2022 22:26:04 +0100 Subject: [PATCH 106/125] add Code of conduct --- CODE_OF_CONDUCT.md | 62 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 CODE_OF_CONDUCT.md diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 000000000..16380faef --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,62 @@ +# Code of Conduct + +Contact: elixir-ls-coc@googlegroups.com + +## Why have a Code of Conduct? + +As contributors and maintainers of this project, we are committed to providing a friendly, safe and welcoming environment for all, regardless of age, disability, gender, nationality, race, religion, sexuality, or similar personal characteristic. + +The goal of the Code of Conduct is to specify a baseline standard of behavior so that people with different social values and communication styles can talk about Elixir effectively, productively, and respectfully, even in face of disagreements. The Code of Conduct also provides a mechanism for resolving conflicts in the community when they arise. + +## Our Values + +These are the values ElixirLS developers should aspire to: + + * Be friendly and welcoming + * Be kind + * Remember that people have varying communication styles and that not everyone is using their native language. (Meaning and tone can be lost in translation.) + * Interpret the arguments of others in good faith, do not seek to disagree. + * When we do disagree, try to understand why. + * Be thoughtful + * Productive communication requires effort. Think about how your words will be interpreted. + * Remember that sometimes it is best to refrain entirely from commenting. + * Be respectful + * In particular, respect differences of opinion. It is important that we resolve disagreements and differing views constructively. + * Be constructive + * Avoid derailing: stay on topic; if you want to talk about something else, start a new conversation. + * Avoid unconstructive criticism: don't merely decry the current state of affairs; offer — or at least solicit — suggestions as to how things may be improved. + * Avoid harsh words and stern tone: we are all aligned towards the well-being of the community and the progress of the ecosystem. Harsh words exclude, demotivate, and lead to unnecessary conflict. + * Avoid snarking (pithy, unproductive, sniping comments). + * Avoid microaggressions (brief and commonplace verbal, behavioral and environmental indignities that communicate hostile, derogatory or negative slights and insults towards a project, person or group). + * Be responsible + * What you say and do matters. Take responsibility for your words and actions, including their consequences, whether intended or otherwise. + +The following actions are explicitly forbidden: + + * Insulting, demeaning, hateful, or threatening remarks. + * Discrimination based on age, disability, gender, nationality, race, religion, sexuality, or similar personal characteristic. + * Bullying or systematic harassment. + * Unwelcome sexual advances. + * Incitement to any of these. + +## Where does the Code of Conduct apply? + +If you participate in or contribute to the ElixirLS in any way, you are encouraged to follow the Code of Conduct while doing so. + +Explicit enforcement of the Code of Conduct applies to the official mediums operated by the ElixirLS project: + +* The [official GitHub projects][1] and code reviews. + +Other ElixirLS activities (such as conferences, meetups, and unofficial forums) are encouraged to adopt this Code of Conduct. Such groups must provide their own contact information. + +Project maintainers may block, remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by emailing: elixir-ls-coc@googlegroups.com. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. **All reports will be kept confidential**. + +**The goal of the Code of Conduct is to resolve conflicts in the most harmonious way possible**. We hope that in most cases issues may be resolved through polite discussion and mutual agreement. Bannings and other forceful measures are to be employed only as a last resort. **Do not** post about the issue publicly or try to rally sentiment against a particular individual or group. + +## Acknowledgements + +This document was based on the Code of Conduct from the Go project (dated Sep/2021) and the Contributor Covenant (v1.4). + +[1]: https://github.com/elixir-lsp/ From badf5dc90e6fc557074d1365ccc0802c85dc3b90 Mon Sep 17 00:00:00 2001 From: Aaron Tinio Date: Wed, 2 Nov 2022 13:48:25 +0800 Subject: [PATCH 107/125] Add components to Phoenix dirs (#757) https://github.com/phoenixframework/phoenix/blob/36639c3132a8063f604dade442238875d787d6af/installer/lib/phx_new/web.ex#L45-L56 --- .../lib/language_server/providers/completion.ex | 3 ++- apps/language_server/test/providers/completion_test.exs | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/language_server/lib/language_server/providers/completion.ex b/apps/language_server/lib/language_server/providers/completion.ex index d968270c0..c4cd2fefd 100644 --- a/apps/language_server/lib/language_server/providers/completion.ex +++ b/apps/language_server/lib/language_server/providers/completion.ex @@ -713,7 +713,8 @@ defmodule ElixirLS.LanguageServer.Providers.Completion do "plugs", "endpoints", "sockets", - "live" + "live", + "components" ] do if String.ends_with?(project_web_dir, "_web") do # by convention Phoenix doesn't use these folders as part of the module names diff --git a/apps/language_server/test/providers/completion_test.exs b/apps/language_server/test/providers/completion_test.exs index 41d2a7c6b..2ccad5bb6 100644 --- a/apps/language_server/test/providers/completion_test.exs +++ b/apps/language_server/test/providers/completion_test.exs @@ -1289,7 +1289,8 @@ defmodule ElixirLS.LanguageServer.Providers.CompletionTest do {"MyProjectWeb.MyChannel", "channels/my_channel.ex"}, {"MyProjectWeb.MyEndpoint", "endpoints/my_endpoint.ex"}, {"MyProjectWeb.MySocket", "sockets/my_socket.ex"}, - {"MyProjectWeb.MyviewLive.MyComponent", "live/myview_live/my_component.ex"} + {"MyProjectWeb.MyviewLive.MyComponent", "live/myview_live/my_component.ex"}, + {"MyProjectWeb.MyComponent", "components/my_component.ex"} ] |> Enum.each(fn {expected_module_name, partial_path} -> path = "some/path/my_project/lib/my_project_web/#{partial_path}" From 3c8a6e0c9d03eac501554f919151c081b09ab945 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=88=91=E6=B2=A1=E6=9C=89=E6=8A=93=E7=8B=82?= Date: Sun, 6 Nov 2022 15:16:52 +0800 Subject: [PATCH 108/125] Fix test code lens when umbrella app names have the same prefix. (#759) --- .../lib/language_server/server.ex | 9 ++- .../umbrella_test_code_lens/apps/app/mix.exs | 14 ++++ .../apps/app/test/fixture_custom_test.exs | 7 ++ .../umbrella_test_code_lens/apps/app1/mix.exs | 14 ++++ .../apps/app1/test/fixture_custom_test.exs | 7 ++ .../fixtures/umbrella_test_code_lens/mix.exs | 7 ++ apps/language_server/test/server_test.exs | 69 +++++++++++++++++++ 7 files changed, 126 insertions(+), 1 deletion(-) create mode 100644 apps/language_server/test/fixtures/umbrella_test_code_lens/apps/app/mix.exs create mode 100644 apps/language_server/test/fixtures/umbrella_test_code_lens/apps/app/test/fixture_custom_test.exs create mode 100644 apps/language_server/test/fixtures/umbrella_test_code_lens/apps/app1/mix.exs create mode 100644 apps/language_server/test/fixtures/umbrella_test_code_lens/apps/app1/test/fixture_custom_test.exs create mode 100644 apps/language_server/test/fixtures/umbrella_test_code_lens/mix.exs diff --git a/apps/language_server/lib/language_server/server.ex b/apps/language_server/lib/language_server/server.ex index b8ea11f5d..900d04e40 100644 --- a/apps/language_server/lib/language_server/server.ex +++ b/apps/language_server/lib/language_server/server.ex @@ -869,7 +869,7 @@ defmodule ElixirLS.LanguageServer.Server do file_path = SourceFile.Path.from_uri(uri) Mix.Project.apps_paths() - |> Enum.find(fn {_app, app_path} -> String.contains?(file_path, app_path) end) + |> Enum.find(fn {_app, app_path} -> under_app?(file_path, project_dir, app_path) end) |> case do nil -> {:ok, []} @@ -929,6 +929,13 @@ defmodule ElixirLS.LanguageServer.Server do |> Enum.any?(&(&1 == file_path)) end + defp under_app?(file_path, project_dir, app_path) do + file_path_list = file_path |> Path.relative_to(project_dir) |> Path.split() + app_path_list = app_path |> Path.split() + + List.starts_with?(file_path_list, app_path_list) + end + # Build defp trigger_build(state = %__MODULE__{project_dir: project_dir}) do diff --git a/apps/language_server/test/fixtures/umbrella_test_code_lens/apps/app/mix.exs b/apps/language_server/test/fixtures/umbrella_test_code_lens/apps/app/mix.exs new file mode 100644 index 000000000..2cce26bb7 --- /dev/null +++ b/apps/language_server/test/fixtures/umbrella_test_code_lens/apps/app/mix.exs @@ -0,0 +1,14 @@ +defmodule App.Mixfile do + use Mix.Project + + def project do + [ + app: :app, + version: "0.1.0" + ] + end + + def application do + [] + end +end diff --git a/apps/language_server/test/fixtures/umbrella_test_code_lens/apps/app/test/fixture_custom_test.exs b/apps/language_server/test/fixtures/umbrella_test_code_lens/apps/app/test/fixture_custom_test.exs new file mode 100644 index 000000000..bf4c51fb2 --- /dev/null +++ b/apps/language_server/test/fixtures/umbrella_test_code_lens/apps/app/test/fixture_custom_test.exs @@ -0,0 +1,7 @@ +defmodule App.UmbrellaTestCodeLensTest do + use ExUnit.Case + + test "fixture test" do + assert true + end +end diff --git a/apps/language_server/test/fixtures/umbrella_test_code_lens/apps/app1/mix.exs b/apps/language_server/test/fixtures/umbrella_test_code_lens/apps/app1/mix.exs new file mode 100644 index 000000000..7bcab07a9 --- /dev/null +++ b/apps/language_server/test/fixtures/umbrella_test_code_lens/apps/app1/mix.exs @@ -0,0 +1,14 @@ +defmodule App1.Mixfile do + use Mix.Project + + def project do + [ + app: :app1, + version: "0.1.0" + ] + end + + def application do + [] + end +end diff --git a/apps/language_server/test/fixtures/umbrella_test_code_lens/apps/app1/test/fixture_custom_test.exs b/apps/language_server/test/fixtures/umbrella_test_code_lens/apps/app1/test/fixture_custom_test.exs new file mode 100644 index 000000000..5b1137b0f --- /dev/null +++ b/apps/language_server/test/fixtures/umbrella_test_code_lens/apps/app1/test/fixture_custom_test.exs @@ -0,0 +1,7 @@ +defmodule App1.UmbrellaTestCodeLensTest do + use ExUnit.Case + + test "fixture test" do + assert true + end +end diff --git a/apps/language_server/test/fixtures/umbrella_test_code_lens/mix.exs b/apps/language_server/test/fixtures/umbrella_test_code_lens/mix.exs new file mode 100644 index 000000000..03459a9e8 --- /dev/null +++ b/apps/language_server/test/fixtures/umbrella_test_code_lens/mix.exs @@ -0,0 +1,7 @@ +defmodule UmbrellaTestCodeLens.Mixfile do + use Mix.Project + + def project do + [apps_path: "apps"] + end +end diff --git a/apps/language_server/test/server_test.exs b/apps/language_server/test/server_test.exs index ca8b3f453..9691081e0 100644 --- a/apps/language_server/test/server_test.exs +++ b/apps/language_server/test/server_test.exs @@ -1287,6 +1287,75 @@ defmodule ElixirLS.LanguageServer.ServerTest do end) end + @tag :fixture + test "returns code lenses for runnable tests in umbrella apps", + %{ + server: server + } do + in_fixture(__DIR__, "umbrella_test_code_lens", fn -> + file_path = "apps/app1/test/fixture_custom_test.exs" + file_uri = SourceFile.Path.to_uri(file_path) + file_absolute_path = SourceFile.Path.from_uri(file_uri) + text = File.read!(file_path) + project_dir = SourceFile.Path.from_uri("#{root_uri()}/apps/app1") + + initialize(server) + + Server.receive_packet( + server, + did_change_configuration(%{"elixirLS" => %{"enableTestLenses" => true}}) + ) + + Server.receive_packet(server, did_open(file_uri, "elixir", 1, text)) + + wait_until_compiled(server) + + Server.receive_packet( + server, + code_lens_req(4, file_uri) + ) + + resp = assert_receive(%{"id" => 4}, 5000) + + assert response(4, [ + %{ + "command" => %{ + "arguments" => [ + %{ + "filePath" => ^file_absolute_path, + "testName" => "fixture test", + "projectDir" => ^project_dir + } + ], + "command" => "elixir.lens.test.run", + "title" => "Run test" + }, + "range" => %{ + "end" => %{"character" => 0, "line" => 3}, + "start" => %{"character" => 0, "line" => 3} + } + }, + %{ + "command" => %{ + "arguments" => [ + %{ + "filePath" => ^file_absolute_path, + "module" => "Elixir.App1.UmbrellaTestCodeLensTest", + "projectDir" => ^project_dir + } + ], + "command" => "elixir.lens.test.run", + "title" => "Run tests in module" + }, + "range" => %{ + "end" => %{"character" => 0, "line" => 0}, + "start" => %{"character" => 0, "line" => 0} + } + } + ]) = resp + end) + end + @tag :fixture test "does not return code lenses for runnable tests when test lenses settings is not set", %{ server: server From 3145fef38de36455f173c460c36d186cefa1fb29 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sun, 6 Nov 2022 08:56:18 +0100 Subject: [PATCH 109/125] bump elixir_sense --- mix.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.lock b/mix.lock index 30f777db5..8fdf9a9c2 100644 --- a/mix.lock +++ b/mix.lock @@ -2,7 +2,7 @@ "dialyxir": {:hex, :dialyxir, "1.1.0", "c5aab0d6e71e5522e77beff7ba9e08f8e02bad90dfbeffae60eaf0cb47e29488", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "07ea8e49c45f15264ebe6d5b93799d4dd56a44036cf42d0ad9c960bc266c0b9a"}, "dialyxir_vendored": {:git, "https://github.com/elixir-lsp/dialyxir.git", "b610d1a87885576e0e2aa4cfb5e176072bcd9457", [branch: "vendored"]}, "docsh": {:hex, :docsh, "0.7.2", "f893d5317a0e14269dd7fe79cf95fb6b9ba23513da0480ec6e77c73221cae4f2", [:rebar3], [{:providers, "1.8.1", [hex: :providers, repo: "hexpm", optional: false]}], "hexpm", "4e7db461bb07540d2bc3d366b8513f0197712d0495bb85744f367d3815076134"}, - "elixir_sense": {:git, "https://github.com/elixir-lsp/elixir_sense.git", "8acc87c901f6fb89b0b18deb85ece6f527876964", []}, + "elixir_sense": {:git, "https://github.com/elixir-lsp/elixir_sense.git", "c26800f600de2afdabc165a9dc8e7b8e91261612", []}, "erl2ex": {:git, "https://github.com/dazuma/erl2ex.git", "244c2d9ed5805ef4855a491d8616b8842fef7ca4", []}, "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, "getopt": {:hex, :getopt, "1.0.1", "c73a9fa687b217f2ff79f68a3b637711bb1936e712b521d8ce466b29cbf7808a", [:rebar3], [], "hexpm", "53e1ab83b9ceb65c9672d3e7a35b8092e9bdc9b3ee80721471a161c10c59959c"}, From edb2e4db5db5d3b6fd12f0c563b549759b2c7808 Mon Sep 17 00:00:00 2001 From: Steve Cohen Date: Sun, 6 Nov 2022 00:56:48 -0700 Subject: [PATCH 110/125] Disable elixir-sense's logging in production (#748) Right now, this change is a no-op, but if and when this change https://github.com/elixir-lsp/elixir_sense/pull/165 lands, this will quiet elixir_sense's logging in the prod environment, which will prevent logspam in lsp buffers when elixir_sense encounters invalid code. --- apps/language_server/lib/language_server/cli.ex | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/language_server/lib/language_server/cli.ex b/apps/language_server/lib/language_server/cli.ex index b5b0c12f8..22fbcefbe 100644 --- a/apps/language_server/lib/language_server/cli.ex +++ b/apps/language_server/lib/language_server/cli.ex @@ -22,6 +22,7 @@ defmodule ElixirLS.LanguageServer.CLI do Launch.start_mix() + Application.put_env(:elixir_sense, :logging_enabled, Mix.env() != :prod) Build.set_compiler_options() start_language_server() From 93c196f2161becfdc200e2e8b4bc950b46a4bef3 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sun, 6 Nov 2022 12:03:09 +0100 Subject: [PATCH 111/125] Improve LSP consistency position should trim to the end of the line --- apps/elixir_ls_debugger/lib/debugger/utils.ex | 5 +--- apps/elixir_ls_debugger/test/utils_test.exs | 12 ++++++++ .../lib/language_server/source_file.ex | 28 ++++++++++++------- .../test/providers/completion_test.exs | 4 +-- .../language_server/test/source_file_test.exs | 22 +++++++++++++++ 5 files changed, 55 insertions(+), 16 deletions(-) diff --git a/apps/elixir_ls_debugger/lib/debugger/utils.ex b/apps/elixir_ls_debugger/lib/debugger/utils.ex index 9ab0eb706..d7bdc7916 100644 --- a/apps/elixir_ls_debugger/lib/debugger/utils.ex +++ b/apps/elixir_ls_debugger/lib/debugger/utils.ex @@ -34,9 +34,6 @@ defmodule ElixirLS.Debugger.Utils do byte_size = byte_size(utf16_line) - # if character index is over the length of the string assume we pad it with spaces (1 byte in utf8) - diff = div(max(dap_character * 2 - byte_size, 0), 2) - utf8_character = utf16_line |> (&binary_part( @@ -47,6 +44,6 @@ defmodule ElixirLS.Debugger.Utils do |> characters_to_binary!(:utf16, :utf8) |> String.length() - utf8_character + diff + utf8_character end end diff --git a/apps/elixir_ls_debugger/test/utils_test.exs b/apps/elixir_ls_debugger/test/utils_test.exs index d2e5b60f0..bb3e8fc89 100644 --- a/apps/elixir_ls_debugger/test/utils_test.exs +++ b/apps/elixir_ls_debugger/test/utils_test.exs @@ -39,6 +39,10 @@ defmodule ElixirLS.Debugger.UtilsTest do assert 0 == Utils.dap_character_to_elixir("", 0) end + test "dap_character_to_elixir empty after end" do + assert 0 == Utils.dap_character_to_elixir("", 1) + end + test "dap_character_to_elixir first char" do assert 0 == Utils.dap_character_to_elixir("abcde", 0) end @@ -47,6 +51,14 @@ defmodule ElixirLS.Debugger.UtilsTest do assert 1 == Utils.dap_character_to_elixir("abcde", 1) end + test "dap_character_to_elixir before line start" do + assert 0 == Utils.dap_character_to_elixir("abcde", -1) + end + + test "dap_character_to_elixir after line end" do + assert 5 == Utils.dap_character_to_elixir("abcde", 15) + end + test "dap_character_to_elixir utf8" do assert 1 == Utils.dap_character_to_elixir("🏳️‍🌈abcde", 6) end diff --git a/apps/language_server/lib/language_server/source_file.ex b/apps/language_server/lib/language_server/source_file.ex index 9cd549f13..5e455b98a 100644 --- a/apps/language_server/lib/language_server/source_file.ex +++ b/apps/language_server/lib/language_server/source_file.ex @@ -283,9 +283,6 @@ defmodule ElixirLS.LanguageServer.SourceFile do byte_size = byte_size(utf16_line) - # if character index is over the length of the string assume we pad it with spaces (1 byte in utf8) - diff = div(max(lsp_character * 2 - byte_size, 0), 2) - utf8_character = utf16_line |> (&binary_part( @@ -296,19 +293,30 @@ defmodule ElixirLS.LanguageServer.SourceFile do |> characters_to_binary!(:utf16, :utf8) |> String.length() - utf8_character + 1 + diff + utf8_character + 1 end + def lsp_position_to_elixir(_urf8_text, {lsp_line, _lsp_character}) when lsp_line < 0, + do: {1, 1} + def lsp_position_to_elixir(_urf8_text, {lsp_line, lsp_character}) when lsp_character <= 0, do: {max(lsp_line + 1, 1), 1} def lsp_position_to_elixir(urf8_text, {lsp_line, lsp_character}) do - utf8_character = - lines(urf8_text) - |> Enum.at(max(lsp_line, 0)) - |> lsp_character_to_elixir(lsp_character) - - {lsp_line + 1, utf8_character} + source_file_lines = lines(urf8_text) + total_lines = length(source_file_lines) + + if lsp_line > total_lines - 1 do + # sanitize to position after last char in last line + {total_lines, String.length(source_file_lines |> Enum.at(total_lines - 1)) + 1} + else + utf8_character = + source_file_lines + |> Enum.at(lsp_line) + |> lsp_character_to_elixir(lsp_character) + + {lsp_line + 1, utf8_character} + end end def elixir_character_to_lsp(_utf8_line, elixir_character) when elixir_character <= 1, do: 0 diff --git a/apps/language_server/test/providers/completion_test.exs b/apps/language_server/test/providers/completion_test.exs index 2ccad5bb6..5f470bafc 100644 --- a/apps/language_server/test/providers/completion_test.exs +++ b/apps/language_server/test/providers/completion_test.exs @@ -476,7 +476,7 @@ defmodule ElixirLS.LanguageServer.Providers.CompletionTest do defstruct [some: nil, other: 1] def dummy_function(var = %MyModule{}) do - %{var | + %{var | # ^ end end @@ -496,7 +496,7 @@ defmodule ElixirLS.LanguageServer.Providers.CompletionTest do text = """ defmodule MyModule do def dummy_function(var = %{some: nil, other: 1}) do - %{var | + %{var | # ^ end end diff --git a/apps/language_server/test/source_file_test.exs b/apps/language_server/test/source_file_test.exs index 3c0937566..54c922934 100644 --- a/apps/language_server/test/source_file_test.exs +++ b/apps/language_server/test/source_file_test.exs @@ -655,6 +655,18 @@ defmodule ElixirLS.LanguageServer.SourceFileTest do assert {1, 2} == SourceFile.lsp_position_to_elixir("abcde", {0, 1}) end + # This is not specified in LSP but some clients fail to synchronize text properly + test "lsp_position_to_elixir single line before line start" do + assert {1, 1} == SourceFile.lsp_position_to_elixir("abcde", {0, -1}) + end + + # LSP spec 3.17 https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#position + # position character If the character value is greater than the line length it defaults back to the line length + test "lsp_position_to_elixir single line after line end" do + assert {1, 6} == SourceFile.lsp_position_to_elixir("abcde", {0, 15}) + assert {1, 1} == SourceFile.lsp_position_to_elixir("", {0, 15}) + end + test "lsp_position_to_elixir single line utf8" do assert {1, 2} == SourceFile.lsp_position_to_elixir("🏳️‍🌈abcde", {0, 6}) end @@ -663,6 +675,16 @@ defmodule ElixirLS.LanguageServer.SourceFileTest do assert {2, 2} == SourceFile.lsp_position_to_elixir("abcde\n1234", {1, 1}) end + # This is not specified in LSP but some clients fail to synchronize text properly + test "lsp_position_to_elixir multi line before first line" do + assert {1, 1} == SourceFile.lsp_position_to_elixir("abcde\n1234", {-1, 2}) + end + + # This is not specified in LSP but some clients fail to synchronize text properly + test "lsp_position_to_elixir multi line after last line" do + assert {2, 5} == SourceFile.lsp_position_to_elixir("abcde\n1234", {8, 2}) + end + test "elixir_position_to_lsp empty" do assert {0, 0} == SourceFile.elixir_position_to_lsp("", {1, 1}) end From aa4dc45ef34b11c98a4e0d258f478a95fdf3a396 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Mon, 7 Nov 2022 21:55:48 +0100 Subject: [PATCH 112/125] Avoid rapid rebuild in test This triggers race conditions in elixir compiler Fixes https://github.com/elixir-lsp/elixir-ls/issues/763 --- apps/language_server/test/server_test.exs | 74 +++++------------------ 1 file changed, 16 insertions(+), 58 deletions(-) diff --git a/apps/language_server/test/server_test.exs b/apps/language_server/test/server_test.exs index 9691081e0..3b9ec9342 100644 --- a/apps/language_server/test/server_test.exs +++ b/apps/language_server/test/server_test.exs @@ -13,14 +13,18 @@ defmodule ElixirLS.LanguageServer.ServerTest do end) end - defp initialize(server) do + defp initialize(server, config \\ nil) do Server.receive_packet(server, initialize_req(1, root_uri(), %{})) Server.receive_packet(server, notification("initialized")) + config = (config || %{}) |> Map.merge(%{"dialyzerEnabled" => false}) + Server.receive_packet( server, - did_change_configuration(%{"elixirLS" => %{"dialyzerEnabled" => false}}) + did_change_configuration(%{"elixirLS" => config}) ) + + wait_until_compiled(server) end defp fake_initialize(server) do @@ -968,8 +972,6 @@ defmodule ElixirLS.LanguageServer.ServerTest do # File is already formatted assert response(3, []) == resp - - wait_until_compiled(server) end) end @@ -1048,8 +1050,6 @@ defmodule ElixirLS.LanguageServer.ServerTest do ] }), 1000 - - wait_until_compiled(server) end) end @@ -1071,8 +1071,6 @@ defmodule ElixirLS.LanguageServer.ServerTest do ] }), 1000 - - wait_until_compiled(server) end) end @@ -1094,8 +1092,6 @@ defmodule ElixirLS.LanguageServer.ServerTest do ] }), 2000 - - wait_until_compiled(server) end) end @@ -1110,7 +1106,7 @@ defmodule ElixirLS.LanguageServer.ServerTest do Build.set_compiler_options() initialize(server) - wait_until_compiled(server) + Server.receive_packet(server, did_open(file_uri, "elixir", 1, text)) Server.receive_packet( @@ -1126,8 +1122,6 @@ defmodule ElixirLS.LanguageServer.ServerTest do "uri" => ^reference_uri } ]) = resp - - wait_until_compiled(server) end) after Code.put_compiler_option(:tracers, []) @@ -1144,7 +1138,7 @@ defmodule ElixirLS.LanguageServer.ServerTest do Build.set_compiler_options() initialize(server) - wait_until_compiled(server) + Server.receive_packet(server, did_open(file_uri, "elixir", 1, text)) Server.receive_packet( @@ -1160,8 +1154,6 @@ defmodule ElixirLS.LanguageServer.ServerTest do "uri" => ^reference_uri } ]) = resp - - wait_until_compiled(server) end) after Code.put_compiler_option(:tracers, []) @@ -1176,7 +1168,6 @@ defmodule ElixirLS.LanguageServer.ServerTest do with_new_server(fn server -> {:ok, _pid} = Tracer.start_link([]) initialize(server) - wait_until_compiled(server) end) # unload App2.Foo @@ -1185,7 +1176,6 @@ defmodule ElixirLS.LanguageServer.ServerTest do # re-visiting the same project with_new_server(fn server -> initialize(server) - wait_until_compiled(server) file_path = "apps/app1/lib/bar.ex" uri = SourceFile.Path.to_uri(file_path) @@ -1230,12 +1220,7 @@ defmodule ElixirLS.LanguageServer.ServerTest do project_dir = SourceFile.Path.absolute_from_uri(root_uri()) - initialize(server) - - Server.receive_packet( - server, - did_change_configuration(%{"elixirLS" => %{"enableTestLenses" => true}}) - ) + initialize(server, %{"enableTestLenses" => true}) Server.receive_packet(server, did_open(file_uri, "elixir", 1, text)) @@ -1282,8 +1267,6 @@ defmodule ElixirLS.LanguageServer.ServerTest do } } ]) = resp - - wait_until_compiled(server) end) end @@ -1299,17 +1282,10 @@ defmodule ElixirLS.LanguageServer.ServerTest do text = File.read!(file_path) project_dir = SourceFile.Path.from_uri("#{root_uri()}/apps/app1") - initialize(server) - - Server.receive_packet( - server, - did_change_configuration(%{"elixirLS" => %{"enableTestLenses" => true}}) - ) + initialize(server, %{"enableTestLenses" => true}) Server.receive_packet(server, did_open(file_uri, "elixir", 1, text)) - wait_until_compiled(server) - Server.receive_packet( server, code_lens_req(4, file_uri) @@ -1391,17 +1367,10 @@ defmodule ElixirLS.LanguageServer.ServerTest do text = File.read!(file_path) project_dir = SourceFile.Path.from_uri(root_uri()) - initialize(server) - - Server.receive_packet( - server, - did_change_configuration(%{"elixirLS" => %{"enableTestLenses" => true}}) - ) + initialize(server, %{"enableTestLenses" => true}) Server.receive_packet(server, did_open(file_uri, "elixir", 1, text)) - wait_until_compiled(server) - Server.receive_packet( server, code_lens_req(4, file_uri) @@ -1460,23 +1429,14 @@ defmodule ElixirLS.LanguageServer.ServerTest do text = File.read!(file_path) project_dir = SourceFile.Path.from_uri("#{root_uri()}/apps/app1") - initialize(server) - - Server.receive_packet( - server, - did_change_configuration(%{ - "elixirLS" => %{ - "enableTestLenses" => true, - "testPaths" => %{"app1" => ["custom_path"]}, - "testPattern" => %{"app1" => "*_custom_test.exs"} - } - }) - ) + initialize(server, %{ + "enableTestLenses" => true, + "testPaths" => %{"app1" => ["custom_path"]}, + "testPattern" => %{"app1" => "*_custom_test.exs"} + }) Server.receive_packet(server, did_open(file_uri, "elixir", 1, text)) - wait_until_compiled(server) - Server.receive_packet( server, code_lens_req(4, file_uri) @@ -1541,8 +1501,6 @@ defmodule ElixirLS.LanguageServer.ServerTest do assert_receive notification("window/logMessage", %{ "message" => "Compile took" <> _ }) - - wait_until_compiled(server) end) end From b27f68b140bb945cf72d7df2e4f43795c35a8253 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Mon, 7 Nov 2022 21:57:06 +0100 Subject: [PATCH 113/125] bump elixir_sense --- mix.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.lock b/mix.lock index 8fdf9a9c2..c59d7578b 100644 --- a/mix.lock +++ b/mix.lock @@ -2,7 +2,7 @@ "dialyxir": {:hex, :dialyxir, "1.1.0", "c5aab0d6e71e5522e77beff7ba9e08f8e02bad90dfbeffae60eaf0cb47e29488", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "07ea8e49c45f15264ebe6d5b93799d4dd56a44036cf42d0ad9c960bc266c0b9a"}, "dialyxir_vendored": {:git, "https://github.com/elixir-lsp/dialyxir.git", "b610d1a87885576e0e2aa4cfb5e176072bcd9457", [branch: "vendored"]}, "docsh": {:hex, :docsh, "0.7.2", "f893d5317a0e14269dd7fe79cf95fb6b9ba23513da0480ec6e77c73221cae4f2", [:rebar3], [{:providers, "1.8.1", [hex: :providers, repo: "hexpm", optional: false]}], "hexpm", "4e7db461bb07540d2bc3d366b8513f0197712d0495bb85744f367d3815076134"}, - "elixir_sense": {:git, "https://github.com/elixir-lsp/elixir_sense.git", "c26800f600de2afdabc165a9dc8e7b8e91261612", []}, + "elixir_sense": {:git, "https://github.com/elixir-lsp/elixir_sense.git", "1bab571b024404e6568645e01bdd91152692c9cc", []}, "erl2ex": {:git, "https://github.com/dazuma/erl2ex.git", "244c2d9ed5805ef4855a491d8616b8842fef7ca4", []}, "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, "getopt": {:hex, :getopt, "1.0.1", "c73a9fa687b217f2ff79f68a3b637711bb1936e712b521d8ce466b29cbf7808a", [:rebar3], [], "hexpm", "53e1ab83b9ceb65c9672d3e7a35b8092e9bdc9b3ee80721471a161c10c59959c"}, From 2d3f272fb50634db6ad36c1de53d0f375cd111ca Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Mon, 7 Nov 2022 21:58:07 +0100 Subject: [PATCH 114/125] bump version --- apps/elixir_ls_debugger/mix.exs | 2 +- apps/elixir_ls_utils/mix.exs | 2 +- apps/language_server/mix.exs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/elixir_ls_debugger/mix.exs b/apps/elixir_ls_debugger/mix.exs index 8cbf24f21..41cbf83f6 100644 --- a/apps/elixir_ls_debugger/mix.exs +++ b/apps/elixir_ls_debugger/mix.exs @@ -4,7 +4,7 @@ defmodule ElixirLS.Debugger.Mixfile do def project do [ app: :elixir_ls_debugger, - version: "0.11.0", + version: "0.12.0", build_path: "../../_build", config_path: "../../config/config.exs", deps_path: "../../deps", diff --git a/apps/elixir_ls_utils/mix.exs b/apps/elixir_ls_utils/mix.exs index b2a02a397..5e86708c6 100644 --- a/apps/elixir_ls_utils/mix.exs +++ b/apps/elixir_ls_utils/mix.exs @@ -4,7 +4,7 @@ defmodule ElixirLS.Utils.Mixfile do def project do [ app: :elixir_ls_utils, - version: "0.11.0", + version: "0.12.0", build_path: "../../_build", config_path: "../../config/config.exs", deps_path: "../../deps", diff --git a/apps/language_server/mix.exs b/apps/language_server/mix.exs index eb276bef3..5491c5ef2 100644 --- a/apps/language_server/mix.exs +++ b/apps/language_server/mix.exs @@ -4,7 +4,7 @@ defmodule ElixirLS.LanguageServer.Mixfile do def project do [ app: :language_server, - version: "0.11.0", + version: "0.12.0", elixir: ">= 1.11.0", build_path: "../../_build", config_path: "../../config/config.exs", From cc85d3860e1a0a2c8fdf8f68bab2d548957826e1 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Mon, 7 Nov 2022 22:55:30 +0100 Subject: [PATCH 115/125] update changelog --- CHANGELOG.md | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 363bb44f4..64a9735d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,33 @@ ### Unreleased +### v0.12.0: 7 November 2022 + +Improvements: + +- Support for list destructuring and comprehension in `for` and `with` expressions. ElixirLS is able to provide completions for destructured list element [Łukasz Samson](https://github.com/elixir-lsp/elixir_sense/pull/168) +- Introduction of compile tracers. ElixirLS now builds a databases basing on compile tracers API available since elixir 1.10. References provider has been rewritten to support tracer database [Łukasz Samson](https://github.com/elixir-lsp/elixir_sense/pull/160), [Łukasz Samson](https://github.com/elixir-lsp/elixir-ls/pull/724) +- Code action prefixing unused variables with `_` [Luca Cervello](https://github.com/elixir-lsp/elixir-ls/pull/718) +- Complete now proposes not aliased modules and adds required `alias` [Ajay](https://github.com/elixir-lsp/elixir-ls/pull/722), [Ajay](https://github.com/elixir-lsp/elixir_sense/pull/155) +- Custom command running mix clean added. Useful when server hits a compilation error [Łukasz Samson](https://github.com/elixir-lsp/elixir-ls/pull/730) +- Custom command returning tests in `.exs` file [Łukasz Samson](https://github.com/elixir-lsp/elixir-ls/pull/753) +- Better handling of Phoenix components [Aaron Tinio](https://github.com/elixir-lsp/elixir-ls/pull/757) +- Test code lense improvements in umbrella apps [我没有抓狂](https://github.com/elixir-lsp/elixir-ls/pull/759) +- Start script improved when `$XDG_CONFIG_HOME` is not set [Sahn Lam](https://github.com/elixir-lsp/elixir-ls/pull/721) +- Deprecated symbols are now deprioretized in completions [Łukasz Samson](https://github.com/elixir-lsp/elixir-ls/commit/036d876484b8a8265cd93af7a14c729a6089af5e) +- Improvements to logging [Łukasz Samson](https://github.com/elixir-lsp/elixir-ls/pull/746) +- Dialyxir is now vendored. This should avert dependency conflicts [Łukasz Samson](https://github.com/elixir-lsp/elixir-ls/commit/84500aedc46ae872f8ad097758a3e90efe75c1fc) +- ElixirLS emits more helpful error messages in case of common problems [Łukasz Samson](https://github.com/elixir-lsp/elixir-ls/commit/685c2fa6ef2b9d1702b34f372a6e479a01646af9), [Łukasz Samson](https://github.com/elixir-lsp/elixir-ls/commit/32e4c2959d8b6ae99d0319aa13be940f3cf1cc15), [Łukasz Samson](https://github.com/elixir-lsp/elixir-ls/commit/d3e05a6c3dfd01d4cdb993edc0cf1d41f77783cf) +- Automatic builds can now be disabled [Hans](https://github.com/elixir-lsp/elixir-ls/pull/440) +- Better module name suggested for `defprotocol` [Milo Lee](https://github.com/elixir-lsp/elixir-ls/pull/715) +- Improved LSP position handling [Łukasz Samson](https://github.com/elixir-lsp/elixir-ls/commit/93c196f2161becfdc200e2e8b4bc950b46a4bef3) + +Fixes: + +- Several crashes with `untitled:` schema URIs fixed [Łukasz Samson](https://github.com/elixir-lsp/elixir-ls/commit/1cf22eee7f126dddedbed55786a6dc63b57d49d8), [Łukasz Samson](https://github.com/elixir-lsp/elixir-ls/commit/65e889a65420d06133acc2e8d2085477b840490b), [Łukasz Samson](https://github.com/elixir-lsp/elixir-ls/commit/dacdcdf47b8feec87d187302d1a880bcd6f54d25) +- Longstanding bug in dependencies reloading leading to infamous `** (Mix.Error) Can't continue due to errors on dependencies` fixed [Łukasz Samson](https://github.com/elixir-lsp/elixir-ls/commit/031bd0a7246dc7f6d6e1077a975b55b2110f209c) +- Fixed crash when formatting a file with syntax errors [Steve Cohen](https://github.com/elixir-lsp/elixir-ls/pull/738) +- Fixed several crashes in document symbols [Steve Cohen](https://github.com/elixir-lsp/elixir-ls/pull/736) + ### v0.11.0: 14 August 2022 Improvements: From 1a1baf2f57c2faf604b7e4f7b8abfb0b5d17c0f8 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Mon, 7 Nov 2022 23:01:29 +0100 Subject: [PATCH 116/125] add elixir 1.14 to release matrix --- .github/workflows/release-asset.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release-asset.yml b/.github/workflows/release-asset.yml index d1bf789a8..1f693eae1 100644 --- a/.github/workflows/release-asset.yml +++ b/.github/workflows/release-asset.yml @@ -32,8 +32,14 @@ jobs: strategy: matrix: include: + - elixir-version: '1.14' + otp-version: '25.1' + - elixir-version: '1.14' + otp-version: '24.3' + - elixir-version: '1.14' + otp-version: '23.3' - elixir-version: '1.13' - otp-version: '25.0' + otp-version: '25.1' - elixir-version: '1.13' otp-version: '24.3' - elixir-version: '1.13' From 77670d5cd60c1931e1355f5fb9d39a5f52d74133 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Mon, 7 Nov 2022 23:11:25 +0100 Subject: [PATCH 117/125] changelog updated --- CHANGELOG.md | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 64a9735d3..da8c33e7a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,29 +4,29 @@ Improvements: -- Support for list destructuring and comprehension in `for` and `with` expressions. ElixirLS is able to provide completions for destructured list element [Łukasz Samson](https://github.com/elixir-lsp/elixir_sense/pull/168) -- Introduction of compile tracers. ElixirLS now builds a databases basing on compile tracers API available since elixir 1.10. References provider has been rewritten to support tracer database [Łukasz Samson](https://github.com/elixir-lsp/elixir_sense/pull/160), [Łukasz Samson](https://github.com/elixir-lsp/elixir-ls/pull/724) -- Code action prefixing unused variables with `_` [Luca Cervello](https://github.com/elixir-lsp/elixir-ls/pull/718) -- Complete now proposes not aliased modules and adds required `alias` [Ajay](https://github.com/elixir-lsp/elixir-ls/pull/722), [Ajay](https://github.com/elixir-lsp/elixir_sense/pull/155) -- Custom command running mix clean added. Useful when server hits a compilation error [Łukasz Samson](https://github.com/elixir-lsp/elixir-ls/pull/730) -- Custom command returning tests in `.exs` file [Łukasz Samson](https://github.com/elixir-lsp/elixir-ls/pull/753) -- Better handling of Phoenix components [Aaron Tinio](https://github.com/elixir-lsp/elixir-ls/pull/757) -- Test code lense improvements in umbrella apps [我没有抓狂](https://github.com/elixir-lsp/elixir-ls/pull/759) -- Start script improved when `$XDG_CONFIG_HOME` is not set [Sahn Lam](https://github.com/elixir-lsp/elixir-ls/pull/721) -- Deprecated symbols are now deprioretized in completions [Łukasz Samson](https://github.com/elixir-lsp/elixir-ls/commit/036d876484b8a8265cd93af7a14c729a6089af5e) -- Improvements to logging [Łukasz Samson](https://github.com/elixir-lsp/elixir-ls/pull/746) -- Dialyxir is now vendored. This should avert dependency conflicts [Łukasz Samson](https://github.com/elixir-lsp/elixir-ls/commit/84500aedc46ae872f8ad097758a3e90efe75c1fc) -- ElixirLS emits more helpful error messages in case of common problems [Łukasz Samson](https://github.com/elixir-lsp/elixir-ls/commit/685c2fa6ef2b9d1702b34f372a6e479a01646af9), [Łukasz Samson](https://github.com/elixir-lsp/elixir-ls/commit/32e4c2959d8b6ae99d0319aa13be940f3cf1cc15), [Łukasz Samson](https://github.com/elixir-lsp/elixir-ls/commit/d3e05a6c3dfd01d4cdb993edc0cf1d41f77783cf) -- Automatic builds can now be disabled [Hans](https://github.com/elixir-lsp/elixir-ls/pull/440) -- Better module name suggested for `defprotocol` [Milo Lee](https://github.com/elixir-lsp/elixir-ls/pull/715) -- Improved LSP position handling [Łukasz Samson](https://github.com/elixir-lsp/elixir-ls/commit/93c196f2161becfdc200e2e8b4bc950b46a4bef3) +- Support for list destructuring and comprehension in `for` and `with` expressions. ElixirLS is able to provide completions for destructured list element +- Introduction of compile tracers. ElixirLS now builds a databases basing on compile tracers API available since elixir 1.10. References provider has been rewritten to support tracer database +- Code action prefixing unused variables with `_` [Luca Cervello](https://github.com/lucacervello) +- Complete now proposes not aliased modules and adds required `alias` [Ajay](https://github.com/ajayvigneshk) +- Custom command running mix clean added. Useful when server hits a compilation error +- Custom command returning tests in `.exs` file +- Better handling of Phoenix components [Aaron Tinio](https://github.com/aptinio) +- Test code lense improvements in umbrella apps [我没有抓狂](https://github.com/BlindingDark) +- Start script improved when `$XDG_CONFIG_HOME` is not set [Sahn Lam](https://github.com/slam) +- Deprecated symbols are now deprioretized in completions +- Improvements to logging +- Dialyxir is now vendored. This should avert dependency conflicts +- ElixirLS emits more helpful error messages in case of common problems +- Automatic builds can now be disabled [Hans](https://github.com/Hanspagh) +- Better module name suggested for `defprotocol` [Milo Lee](https://github.com/oo6) +- Improved LSP position handling Fixes: -- Several crashes with `untitled:` schema URIs fixed [Łukasz Samson](https://github.com/elixir-lsp/elixir-ls/commit/1cf22eee7f126dddedbed55786a6dc63b57d49d8), [Łukasz Samson](https://github.com/elixir-lsp/elixir-ls/commit/65e889a65420d06133acc2e8d2085477b840490b), [Łukasz Samson](https://github.com/elixir-lsp/elixir-ls/commit/dacdcdf47b8feec87d187302d1a880bcd6f54d25) -- Longstanding bug in dependencies reloading leading to infamous `** (Mix.Error) Can't continue due to errors on dependencies` fixed [Łukasz Samson](https://github.com/elixir-lsp/elixir-ls/commit/031bd0a7246dc7f6d6e1077a975b55b2110f209c) -- Fixed crash when formatting a file with syntax errors [Steve Cohen](https://github.com/elixir-lsp/elixir-ls/pull/738) -- Fixed several crashes in document symbols [Steve Cohen](https://github.com/elixir-lsp/elixir-ls/pull/736) +- Several crashes with `untitled:` schema URIs fixed +- Longstanding bug in dependencies reloading leading to infamous `** (Mix.Error) Can't continue due to errors on dependencies` fixed +- Fixed crash when formatting a file with syntax errors [Steve Cohen](https://github.com/scohen) +- Fixed several crashes in document symbols [Steve Cohen](https://github.com/scohen) ### v0.11.0: 14 August 2022 From 4171632aca5ebcee341aff8051749c82acb2a2ed Mon Sep 17 00:00:00 2001 From: Tim Gent Date: Thu, 10 Nov 2022 15:42:02 +0000 Subject: [PATCH 118/125] Fix rename file version & file paths --- .../lib/language_server/providers/rename.ex | 10 ++++++---- apps/language_server/test/providers/rename_test.exs | 12 ++++++------ 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/apps/language_server/lib/language_server/providers/rename.ex b/apps/language_server/lib/language_server/providers/rename.ex index 2897e9d9b..9333229f3 100644 --- a/apps/language_server/lib/language_server/providers/rename.ex +++ b/apps/language_server/lib/language_server/providers/rename.ex @@ -49,7 +49,7 @@ defmodule ElixirLS.LanguageServer.Providers.Rename do %{ "textDocument" => %{ "uri" => uri, - "version" => source_file.version + 1 + "version" => nil }, "edits" => Enum.map(edits, fn edit -> @@ -83,8 +83,10 @@ defmodule ElixirLS.LanguageServer.Providers.Rename do {:ok, result} end - defp repack_references(references, uri) do + defp repack_references(references, start_uri) do for reference <- references do + uri = if reference.uri, do: SourceFile.path_to_uri(reference.uri), else: start_uri + %{ uri: uri, range: %{ @@ -99,11 +101,11 @@ defmodule ElixirLS.LanguageServer.Providers.Rename do end defp parse_definition_source_code(%{file: file}) do - ElixirSense.Core.Parser.parse_file(file, true, true, 0) + ElixirSense.Core.Parser.parse_file(file, true, true, nil) end defp parse_definition_source_code(source_text) when is_binary(source_text) do - ElixirSense.Core.Parser.parse_string(source_text, true, true, 0) + ElixirSense.Core.Parser.parse_string(source_text, true, true, nil) end defp get_all_fn_header_positions(parsed_source, char_ident) do diff --git a/apps/language_server/test/providers/rename_test.exs b/apps/language_server/test/providers/rename_test.exs index 934ac38a3..7afce1c8d 100644 --- a/apps/language_server/test/providers/rename_test.exs +++ b/apps/language_server/test/providers/rename_test.exs @@ -38,7 +38,7 @@ defmodule ElixirLS.LanguageServer.Providers.RenameTest do edits = Rename.rename(%SourceFile{text: text, version: 0}, @fake_uri, line, char, "test") - |> assert_return_structure_and_get_edits(@fake_uri, 1) + |> assert_return_structure_and_get_edits(@fake_uri, nil) expected_edits = [ @@ -70,7 +70,7 @@ defmodule ElixirLS.LanguageServer.Providers.RenameTest do char, "name" ) - |> assert_return_structure_and_get_edits(@fake_uri, 1) + |> assert_return_structure_and_get_edits(@fake_uri, nil) expected_edits = [ @@ -100,7 +100,7 @@ defmodule ElixirLS.LanguageServer.Providers.RenameTest do char, "new_subtract" ) - |> assert_return_structure_and_get_edits(uri, 1) + |> assert_return_structure_and_get_edits(uri, nil) expected_edits = [ @@ -128,7 +128,7 @@ defmodule ElixirLS.LanguageServer.Providers.RenameTest do char, "new_add" ) - |> assert_return_structure_and_get_edits(uri, 1) + |> assert_return_structure_and_get_edits(uri, nil) expected_edits = [ @@ -160,14 +160,14 @@ defmodule ElixirLS.LanguageServer.Providers.RenameTest do %{ "textDocument" => %{ "uri" => ^uri, - "version" => 1 + "version" => nil }, "edits" => file_edits }, %{ "textDocument" => %{ "uri" => ^fn_definition_file_uri, - "version" => 1 + "version" => nil }, "edits" => fn_definition_file_edits } From 0df099669d3a95843af1fc5c6650124e206af317 Mon Sep 17 00:00:00 2001 From: Tim Gent Date: Thu, 10 Nov 2022 16:04:58 +0000 Subject: [PATCH 119/125] Fix rename issue with duplicate entries when renaming var definition --- .../lib/language_server/providers/rename.ex | 2 +- .../test/providers/rename_test.exs | 33 +++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/apps/language_server/lib/language_server/providers/rename.ex b/apps/language_server/lib/language_server/providers/rename.ex index 9333229f3..4b87e2a42 100644 --- a/apps/language_server/lib/language_server/providers/rename.ex +++ b/apps/language_server/lib/language_server/providers/rename.ex @@ -36,7 +36,7 @@ defmodule ElixirLS.LanguageServer.Providers.Rename do ) end - definition_references ++ repack_references(references, start_uri) + Enum.uniq(definition_references ++ repack_references(references, start_uri)) else _ -> [] diff --git a/apps/language_server/test/providers/rename_test.exs b/apps/language_server/test/providers/rename_test.exs index 7afce1c8d..74ff62b16 100644 --- a/apps/language_server/test/providers/rename_test.exs +++ b/apps/language_server/test/providers/rename_test.exs @@ -81,6 +81,39 @@ defmodule ElixirLS.LanguageServer.Providers.RenameTest do assert sort_edit_by_start_line(edits) == expected_edits end + + test "renaming a variable definition works original -> new_original" do + text = """ + defmodule MyModule do + def hello do + original = "original" + new = original <> " new stuff" + end + end + """ + + # new = "#{original} + new stuff!" + {line, char} = {3, 6} + + edits = + Rename.rename( + %SourceFile{text: text, version: 0}, + @fake_uri, + line, + char, + "new_original" + ) + |> assert_return_structure_and_get_edits(@fake_uri, nil) + + expected_edits = + [ + %{line: 2, start_char: 4, end_char: 12}, + %{line: 3, start_char: 10, end_char: 18} + ] + |> get_expected_edits("new_original") + + assert sort_edit_by_start_line(edits) == expected_edits + end end describe "renaming local function" do From 04407d30ddede1affffb40ef04e0cf0d6781a34f Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sat, 12 Nov 2022 16:37:14 +0100 Subject: [PATCH 120/125] drop support for elixir 1.11 --- .github/workflows/ci.yml | 32 +++++++------------ .github/workflows/release-asset.yml | 6 ---- .release-tool-versions | 2 +- CHANGELOG.md | 3 ++ apps/elixir_ls_debugger/mix.exs | 2 +- apps/elixir_ls_utils/lib/minimum_version.ex | 4 +-- apps/elixir_ls_utils/mix.exs | 2 +- .../lib/language_server/tracer.ex | 10 ++---- apps/language_server/mix.exs | 2 +- mix.exs | 2 +- mix.lock | 2 +- 11 files changed, 25 insertions(+), 42 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 46338b99c..1b0a697f3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,36 +16,26 @@ jobs: fail-fast: false matrix: include: - - elixir: 1.11.x - otp: 22.3.x - tests_may_fail: false - - elixir: 1.11.x - otp: 23.3.x - tests_may_fail: false - elixir: 1.12.x - otp: 23.3.x - tests_may_fail: false + otp: 22.x + - elixir: 1.12.x + otp: 23.x + - elixir: 1.12.x + otp: 24.x - elixir: 1.13.x - otp: 22.3.x - tests_may_fail: false + otp: 22.x - elixir: 1.13.x - otp: 23.3.x - tests_may_fail: false + otp: 23.x - elixir: 1.13.x - otp: 24.3.x - tests_may_fail: false + otp: 24.x - elixir: 1.13.x otp: 25.x - tests_may_fail: false - elixir: 1.14.x - otp: 23.3.x - tests_may_fail: false + otp: 23.x - elixir: 1.14.x - otp: 24.3.x - tests_may_fail: false + otp: 24.x - elixir: 1.14.x otp: 25.x - tests_may_fail: false env: MIX_ENV: test steps: @@ -59,7 +49,7 @@ jobs: mix local.hex --force mix local.rebar --force mix deps.get --only test - - run: mix test || ${{ matrix.tests_may_fail }} + - run: mix test static_analysis: name: static analysis (Elixir ${{matrix.elixir}} | Erlang/OTP ${{matrix.otp}}) diff --git a/.github/workflows/release-asset.yml b/.github/workflows/release-asset.yml index 1f693eae1..57dce27ae 100644 --- a/.github/workflows/release-asset.yml +++ b/.github/workflows/release-asset.yml @@ -52,12 +52,6 @@ jobs: otp-version: '23.3' - elixir-version: '1.12' otp-version: '22.3' - - elixir-version: '1.11' - otp-version: '24.3' - - elixir-version: '1.11' - otp-version: '23.3' - - elixir-version: '1.11' - otp-version: '22.3' default: true steps: diff --git a/.release-tool-versions b/.release-tool-versions index b87c5fe43..09c810961 100644 --- a/.release-tool-versions +++ b/.release-tool-versions @@ -3,5 +3,5 @@ # # The versions selected here are the versions that are used to build a binary # release for distribution -elixir 1.11.4-otp-22 +elixir 1.12.3-otp-22 erlang 22.3.4.20 diff --git a/CHANGELOG.md b/CHANGELOG.md index da8c33e7a..a2ce6b528 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ ### Unreleased +**Deprecations** +- Minimum version of Elixir is now 1.12.3 + ### v0.12.0: 7 November 2022 Improvements: diff --git a/apps/elixir_ls_debugger/mix.exs b/apps/elixir_ls_debugger/mix.exs index 41cbf83f6..ab339fe38 100644 --- a/apps/elixir_ls_debugger/mix.exs +++ b/apps/elixir_ls_debugger/mix.exs @@ -9,7 +9,7 @@ defmodule ElixirLS.Debugger.Mixfile do config_path: "../../config/config.exs", deps_path: "../../deps", lockfile: "../../mix.lock", - elixir: ">= 1.11.0", + elixir: ">= 1.12.3", build_embedded: false, start_permanent: true, build_per_environment: false, diff --git a/apps/elixir_ls_utils/lib/minimum_version.ex b/apps/elixir_ls_utils/lib/minimum_version.ex index 0d7421339..852c29ef1 100644 --- a/apps/elixir_ls_utils/lib/minimum_version.ex +++ b/apps/elixir_ls_utils/lib/minimum_version.ex @@ -11,11 +11,11 @@ defmodule ElixirLS.Utils.MinimumVersion do end def check_elixir_version do - if Version.match?(System.version(), ">= 1.11.0") do + if Version.match?(System.version(), ">= 1.12.3") do :ok else {:error, - "Elixir versions below 1.11 are not supported. (Currently running v#{System.version()})"} + "Elixir versions below 1.12.3 are not supported. (Currently running v#{System.version()})"} end end end diff --git a/apps/elixir_ls_utils/mix.exs b/apps/elixir_ls_utils/mix.exs index 5e86708c6..b6a01fcd4 100644 --- a/apps/elixir_ls_utils/mix.exs +++ b/apps/elixir_ls_utils/mix.exs @@ -10,7 +10,7 @@ defmodule ElixirLS.Utils.Mixfile do deps_path: "../../deps", elixirc_paths: elixirc_paths(Mix.env()), lockfile: "../../mix.lock", - elixir: ">= 1.11.0", + elixir: ">= 1.12.3", build_embedded: false, start_permanent: false, build_per_environment: false, diff --git a/apps/language_server/lib/language_server/tracer.ex b/apps/language_server/lib/language_server/tracer.ex index ebdb8c156..81f362467 100644 --- a/apps/language_server/lib/language_server/tracer.ex +++ b/apps/language_server/lib/language_server/tracer.ex @@ -206,13 +206,9 @@ defmodule ElixirLS.LanguageServer.Tracer do defp build_module_info(module, file, line) do defs = - if Version.match?(System.version(), ">= 1.12.0") do - for {name, arity} <- Module.definitions_in(module) do - def_info = apply(Module, :get_definition, [module, {name, arity}]) - {{name, arity}, build_def_info(def_info)} - end - else - [] + for {name, arity} <- Module.definitions_in(module) do + def_info = apply(Module, :get_definition, [module, {name, arity}]) + {{name, arity}, build_def_info(def_info)} end attributes = diff --git a/apps/language_server/mix.exs b/apps/language_server/mix.exs index 5491c5ef2..386f6b967 100644 --- a/apps/language_server/mix.exs +++ b/apps/language_server/mix.exs @@ -5,7 +5,7 @@ defmodule ElixirLS.LanguageServer.Mixfile do [ app: :language_server, version: "0.12.0", - elixir: ">= 1.11.0", + elixir: ">= 1.12.3", build_path: "../../_build", config_path: "../../config/config.exs", deps_path: "../../deps", diff --git a/mix.exs b/mix.exs index 2417b94b9..ec5cb2450 100644 --- a/mix.exs +++ b/mix.exs @@ -9,7 +9,7 @@ defmodule ElixirLS.Mixfile do start_permanent: Mix.env() == :prod, build_per_environment: false, deps: deps(), - elixir: ">= 1.11.0", + elixir: ">= 1.12.3", dialyzer: [ plt_add_apps: [:dialyxir_vendored, :debugger, :dialyzer, :ex_unit], flags: [ diff --git a/mix.lock b/mix.lock index c59d7578b..01824dd8a 100644 --- a/mix.lock +++ b/mix.lock @@ -2,7 +2,7 @@ "dialyxir": {:hex, :dialyxir, "1.1.0", "c5aab0d6e71e5522e77beff7ba9e08f8e02bad90dfbeffae60eaf0cb47e29488", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "07ea8e49c45f15264ebe6d5b93799d4dd56a44036cf42d0ad9c960bc266c0b9a"}, "dialyxir_vendored": {:git, "https://github.com/elixir-lsp/dialyxir.git", "b610d1a87885576e0e2aa4cfb5e176072bcd9457", [branch: "vendored"]}, "docsh": {:hex, :docsh, "0.7.2", "f893d5317a0e14269dd7fe79cf95fb6b9ba23513da0480ec6e77c73221cae4f2", [:rebar3], [{:providers, "1.8.1", [hex: :providers, repo: "hexpm", optional: false]}], "hexpm", "4e7db461bb07540d2bc3d366b8513f0197712d0495bb85744f367d3815076134"}, - "elixir_sense": {:git, "https://github.com/elixir-lsp/elixir_sense.git", "1bab571b024404e6568645e01bdd91152692c9cc", []}, + "elixir_sense": {:git, "https://github.com/elixir-lsp/elixir_sense.git", "ea9eadbec00edef3b98f5389c509ddb81ffcacd0", []}, "erl2ex": {:git, "https://github.com/dazuma/erl2ex.git", "244c2d9ed5805ef4855a491d8616b8842fef7ca4", []}, "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, "getopt": {:hex, :getopt, "1.0.1", "c73a9fa687b217f2ff79f68a3b637711bb1936e712b521d8ce466b29cbf7808a", [:rebar3], [], "hexpm", "53e1ab83b9ceb65c9672d3e7a35b8092e9bdc9b3ee80721471a161c10c59959c"}, From 2984b662c7552de3995555523fb149aa901104d6 Mon Sep 17 00:00:00 2001 From: Travis Vander Hoop Date: Sun, 13 Nov 2022 01:47:16 -0700 Subject: [PATCH 121/125] Add regression test for autocompletion of 'fn' within parentheses (#768) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - so, this behavior wasn't working for me in vs-code, so I hopped into the repo to write a failing test, but the test passed right away (🤔) and I'm now seeing the behavior _sometimes_ work and sometimes not in vs-code - conclusion is that the issue is likely in the vs-code extension, but I figured I'd submit a PR with the regression test if it's desired --- .../test/providers/completion_test.exs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/apps/language_server/test/providers/completion_test.exs b/apps/language_server/test/providers/completion_test.exs index 5f470bafc..134e67bd9 100644 --- a/apps/language_server/test/providers/completion_test.exs +++ b/apps/language_server/test/providers/completion_test.exs @@ -128,6 +128,24 @@ defmodule ElixirLS.LanguageServer.Providers.CompletionTest do end end + test "returns fn autocompletion when inside parentheses" do + text = """ + defmodule MyModule do + + def dummy_function() do + Task.async(fn) + # ^ + end + end + """ + + {line, char} = {3, 17} + TestUtils.assert_has_cursor_char(text, line, char) + {:ok, %{"items" => [first_suggestion | _tail]}} = Completion.completion(text, line, char, @supports) + + assert first_suggestion["label"] === "fn" + end + test "unless with snippets not supported does not return a completion" do text = """ defmodule MyModule do From 42c4d30273379532b260b0f475691d735e3d35cb Mon Sep 17 00:00:00 2001 From: Tim Gent Date: Sun, 13 Nov 2022 21:18:42 +0000 Subject: [PATCH 122/125] Tests for rename prepare and fix rename prepare across multiple files --- .../lib/language_server/providers/rename.ex | 22 +++++- .../test/providers/rename_test.exs | 78 +++++++++++++++---- 2 files changed, 80 insertions(+), 20 deletions(-) diff --git a/apps/language_server/lib/language_server/providers/rename.ex b/apps/language_server/lib/language_server/providers/rename.ex index 4b87e2a42..225efd060 100644 --- a/apps/language_server/lib/language_server/providers/rename.ex +++ b/apps/language_server/lib/language_server/providers/rename.ex @@ -66,10 +66,10 @@ defmodule ElixirLS.LanguageServer.Providers.Rename do with %{ begin: {start_line, start_col}, end: {end_line, end_col}, - context: {context, char_ident} - } - when context in [:local_or_var, :local_call] <- - Code.Fragment.surround_context(source_file.text, {line, character}) do + char_ident: char_ident + } = res + when not is_nil(res) <- + get_begin_end_and_char_ident(source_file.text, line, character) do %{ range: adjust_range(start_line, start_col, end_line, end_col), placeholder: to_string(char_ident) @@ -142,4 +142,18 @@ defmodule ElixirLS.LanguageServer.Providers.Rename do _ -> nil end end + + defp get_begin_end_and_char_ident(text, line, character) do + case Code.Fragment.surround_context(text, {line, character}) do + %{begin: begin, end: the_end, context: {context, char_ident}} + when context in [:local_or_var, :local_call] -> + %{begin: begin, end: the_end, char_ident: char_ident} + + %{begin: begin, end: the_end, context: {:dot, _, char_ident}} -> + %{begin: begin, end: the_end, char_ident: char_ident} + + _ -> + nil + end + end end diff --git a/apps/language_server/test/providers/rename_test.exs b/apps/language_server/test/providers/rename_test.exs index 74ff62b16..14f4983f5 100644 --- a/apps/language_server/test/providers/rename_test.exs +++ b/apps/language_server/test/providers/rename_test.exs @@ -36,14 +36,20 @@ defmodule ElixirLS.LanguageServer.Providers.RenameTest do # _a + b {line, char} = {3, 5} + source = %SourceFile{text: text, version: 0} + target_range = %{line: 2, start_char: 4, end_char: 5} + + Rename.prepare(source, @fake_uri, line, char) + |> assert_prepare_range_and_placeholder_is(target_range, "a") + edits = - Rename.rename(%SourceFile{text: text, version: 0}, @fake_uri, line, char, "test") + Rename.rename(source, @fake_uri, line, char, "test") |> assert_return_structure_and_get_edits(@fake_uri, nil) expected_edits = [ %{line: 1, start_char: 10, end_char: 11}, - %{line: 2, start_char: 4, end_char: 5} + target_range ] |> get_expected_edits("test") @@ -62,9 +68,15 @@ defmodule ElixirLS.LanguageServer.Providers.RenameTest do # "Hello " <> ne_ma {line, char} = {3, 19} + source = %SourceFile{text: text, version: 0} + target_range = %{line: 2, start_char: 16, end_char: 20} + + Rename.prepare(source, @fake_uri, line, char) + |> assert_prepare_range_and_placeholder_is(target_range, "nema") + edits = Rename.rename( - %SourceFile{text: text, version: 0}, + source, @fake_uri, line, char, @@ -75,7 +87,7 @@ defmodule ElixirLS.LanguageServer.Providers.RenameTest do expected_edits = [ %{line: 1, start_char: 12, end_char: 16}, - %{line: 2, start_char: 16, end_char: 20} + target_range ] |> get_expected_edits("name") @@ -94,10 +106,15 @@ defmodule ElixirLS.LanguageServer.Providers.RenameTest do # new = "#{original} + new stuff!" {line, char} = {3, 6} + source = %SourceFile{text: text, version: 0} + target_range = %{line: 2, start_char: 4, end_char: 12} + + Rename.prepare(source, @fake_uri, line, char) + |> assert_prepare_range_and_placeholder_is(target_range, "original") edits = Rename.rename( - %SourceFile{text: text, version: 0}, + source, @fake_uri, line, char, @@ -107,7 +124,7 @@ defmodule ElixirLS.LanguageServer.Providers.RenameTest do expected_edits = [ - %{line: 2, start_char: 4, end_char: 12}, + target_range, %{line: 3, start_char: 10, end_char: 18} ] |> get_expected_edits("new_original") @@ -120,14 +137,19 @@ defmodule ElixirLS.LanguageServer.Providers.RenameTest do test "subtract -> new_subtract" do file_path = FixtureHelpers.get_path("rename_example.ex") text = File.read!(file_path) - uri = SourceFile.path_to_uri(file_path) + uri = SourceFile.Path.to_uri(file_path) # d = subtract(a, b) {line, char} = {6, 10} + source = %SourceFile{text: text, version: 0} + target_range = %{line: 5, start_char: 8, end_char: 16} + + Rename.prepare(source, @fake_uri, line, char) + |> assert_prepare_range_and_placeholder_is(target_range, "subtract") edits = Rename.rename( - %SourceFile{text: text, version: 0}, + source, uri, line, char, @@ -137,7 +159,7 @@ defmodule ElixirLS.LanguageServer.Providers.RenameTest do expected_edits = [ - %{line: 5, start_char: 8, end_char: 16}, + target_range, %{line: 13, start_char: 7, end_char: 15} ] |> get_expected_edits("new_subtract") @@ -148,14 +170,19 @@ defmodule ElixirLS.LanguageServer.Providers.RenameTest do test "rename function with multiple heads: add -> new_add" do file_path = FixtureHelpers.get_path("rename_example.ex") text = File.read!(file_path) - uri = SourceFile.path_to_uri(file_path) + uri = SourceFile.Path.to_uri(file_path) # c = add(a, b) {line, char} = {5, 9} + source = %SourceFile{text: text, version: 0} + target_range = %{line: 4, start_char: 8, end_char: 11} + + Rename.prepare(source, @fake_uri, line, char) + |> assert_prepare_range_and_placeholder_is(target_range, "add") edits = Rename.rename( - %SourceFile{text: text, version: 0}, + source, uri, line, char, @@ -165,7 +192,7 @@ defmodule ElixirLS.LanguageServer.Providers.RenameTest do expected_edits = [ - %{line: 4, start_char: 8, end_char: 11}, + target_range, %{line: 6, start_char: 4, end_char: 7}, %{line: 9, start_char: 7, end_char: 10}, %{line: 10, start_char: 7, end_char: 10}, @@ -179,13 +206,17 @@ defmodule ElixirLS.LanguageServer.Providers.RenameTest do test "rename function defined in a different file ten -> new_ten" do file_path = FixtureHelpers.get_path("rename_example.ex") text = File.read!(file_path) - uri = SourceFile.path_to_uri(file_path) + uri = SourceFile.Path.to_uri(file_path) fn_definition_file_uri = - FixtureHelpers.get_path("rename_example_b.ex") |> SourceFile.path_to_uri() + FixtureHelpers.get_path("rename_example_b.ex") |> SourceFile.Path.to_uri() # b = ElixirLS.Test.RenameExampleB.ten() {line, char} = {4, 38} + source = %SourceFile{text: text, version: 0} + + Rename.prepare(source, uri, line, char) + |> assert_prepare_range_and_placeholder_is(%{line: 3, start_char: 8, end_char: 40}, "ten") assert {:ok, %{ @@ -207,7 +238,7 @@ defmodule ElixirLS.LanguageServer.Providers.RenameTest do ] }} = Rename.rename( - %SourceFile{text: text, version: 0}, + source, uri, line, char, @@ -231,7 +262,7 @@ defmodule ElixirLS.LanguageServer.Providers.RenameTest do test "rename started with cursor at function definition" do file_path = FixtureHelpers.get_path("rename_example.ex") text = File.read!(file_path) - uri = SourceFile.path_to_uri(file_path) + uri = SourceFile.Path.to_uri(file_path) # defp _handle_error({:ok, message}) {line, char} = {4, 8} @@ -301,4 +332,19 @@ defmodule ElixirLS.LanguageServer.Providers.RenameTest do defp sort_edit_by_start_line(edits) do Enum.sort(edits, &(&1["range"].start.line < &2["range"].start.line)) end + + defp assert_prepare_range_and_placeholder_is( + prepare_result, + %{line: line, start_char: start_char, end_char: end_char} = _expected_range, + expected_placeholder + ) do + assert {:ok, + %{ + placeholder: expected_placeholder, + range: %{ + start: %{line: line, character: start_char}, + end: %{line: line, character: end_char} + } + }} == prepare_result + end end From faf8d25115f9c4a952e5857c962df3b244bb80c0 Mon Sep 17 00:00:00 2001 From: Tim Gent Date: Sun, 13 Nov 2022 21:53:27 +0000 Subject: [PATCH 123/125] Fix rename issues following merge with master --- .../lib/language_server/providers/rename.ex | 8 +++--- .../test/providers/rename_test.exs | 26 +++++++++++++++++++ 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/apps/language_server/lib/language_server/providers/rename.ex b/apps/language_server/lib/language_server/providers/rename.ex index 225efd060..98d8bc732 100644 --- a/apps/language_server/lib/language_server/providers/rename.ex +++ b/apps/language_server/lib/language_server/providers/rename.ex @@ -8,12 +8,14 @@ defmodule ElixirLS.LanguageServer.Providers.Rename do alias ElixirLS.LanguageServer.SourceFile def rename(%SourceFile{} = source_file, start_uri, line, character, new_name) do + trace = ElixirLS.LanguageServer.Tracer.get_trace() + edits = with char_ident when not is_nil(char_ident) <- get_char_ident(source_file.text, line, character), %ElixirSense.Location{} = definition <- ElixirSense.definition(source_file.text, line, character), - references <- ElixirSense.references(source_file.text, line, character) do + references <- ElixirSense.references(source_file.text, line, character, trace) do length_old = length(char_ident) definition_references = @@ -26,7 +28,7 @@ defmodule ElixirLS.LanguageServer.Providers.Rename do %{file: separate_file_path, type: :function} -> parse_definition_source_code(definition) |> get_all_fn_header_positions(char_ident) - |> positions_to_references(SourceFile.path_to_uri(separate_file_path), length_old) + |> positions_to_references(SourceFile.Path.to_uri(separate_file_path), length_old) _ -> positions_to_references( @@ -85,7 +87,7 @@ defmodule ElixirLS.LanguageServer.Providers.Rename do defp repack_references(references, start_uri) do for reference <- references do - uri = if reference.uri, do: SourceFile.path_to_uri(reference.uri), else: start_uri + uri = if reference.uri, do: SourceFile.Path.to_uri(reference.uri), else: start_uri %{ uri: uri, diff --git a/apps/language_server/test/providers/rename_test.exs b/apps/language_server/test/providers/rename_test.exs index 14f4983f5..8f63d26f7 100644 --- a/apps/language_server/test/providers/rename_test.exs +++ b/apps/language_server/test/providers/rename_test.exs @@ -1,13 +1,39 @@ defmodule ElixirLS.LanguageServer.Providers.RenameTest do use ExUnit.Case, async: true + alias ElixirLS.LanguageServer.Build alias ElixirLS.LanguageServer.Providers.Rename alias ElixirLS.LanguageServer.SourceFile alias ElixirLS.LanguageServer.Test.FixtureHelpers + alias ElixirLS.LanguageServer.Tracer # mix cmd --app language_server mix test test/providers/rename_test.exs @fake_uri "file:///World/Netherlands/Amsterdam/supercomputer/amazing.ex" + setup_all context do + File.rm_rf!(FixtureHelpers.get_path(".elixir_ls/calls.dets")) + {:ok, pid} = Tracer.start_link([]) + Tracer.set_project_dir(FixtureHelpers.get_path("")) + + compiler_options = Code.compiler_options() + Build.set_compiler_options(ignore_module_conflict: true) + + on_exit(fn -> + Process.monitor(pid) + Process.unlink(pid) + GenServer.stop(pid) + + receive do + {:DOWN, _, _, _, _} -> :ok + end + end) + + Code.compile_file(FixtureHelpers.get_path("rename_example.ex")) + Code.compile_file(FixtureHelpers.get_path("rename_example_b.ex")) + + {:ok, context} + end + test "rename blank space" do text = """ defmodule MyModule do From fab20320cb44a70417ce688a444ce0b2fb64e285 Mon Sep 17 00:00:00 2001 From: Tim Gent Date: Tue, 15 Nov 2022 21:05:13 +0000 Subject: [PATCH 124/125] Rename to only rename fn headers of correct arity --- .../lib/language_server/providers/rename.ex | 20 ++++++++++++------- .../test/providers/rename_test.exs | 2 +- .../test/support/fixtures/rename_example.ex | 2 ++ 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/apps/language_server/lib/language_server/providers/rename.ex b/apps/language_server/lib/language_server/providers/rename.ex index 98d8bc732..d4588009b 100644 --- a/apps/language_server/lib/language_server/providers/rename.ex +++ b/apps/language_server/lib/language_server/providers/rename.ex @@ -22,12 +22,12 @@ defmodule ElixirLS.LanguageServer.Providers.Rename do case definition do %{file: nil, type: :function} -> parse_definition_source_code(source_file.text) - |> get_all_fn_header_positions(char_ident) + |> get_all_fn_header_positions(char_ident, definition) |> positions_to_references(start_uri, length_old) %{file: separate_file_path, type: :function} -> parse_definition_source_code(definition) - |> get_all_fn_header_positions(char_ident) + |> get_all_fn_header_positions(char_ident, definition) |> positions_to_references(SourceFile.Path.to_uri(separate_file_path), length_old) _ -> @@ -78,7 +78,7 @@ defmodule ElixirLS.LanguageServer.Providers.Rename do } else _ -> - # Not a variable or local call, skipping for now + # Not a variable or function call, skipping nil end @@ -86,7 +86,7 @@ defmodule ElixirLS.LanguageServer.Providers.Rename do end defp repack_references(references, start_uri) do - for reference <- references do + Enum.map(references, fn reference -> uri = if reference.uri, do: SourceFile.Path.to_uri(reference.uri), else: start_uri %{ @@ -99,7 +99,7 @@ defmodule ElixirLS.LanguageServer.Providers.Rename do } } } - end + end) end defp parse_definition_source_code(%{file: file}) do @@ -110,10 +110,16 @@ defmodule ElixirLS.LanguageServer.Providers.Rename do ElixirSense.Core.Parser.parse_string(source_text, true, true, nil) end - defp get_all_fn_header_positions(parsed_source, char_ident) do + defp get_all_fn_header_positions( + parsed_source, + definition_name, + %{column: column, line: line} = _definition + ) do parsed_source.mods_funs_to_positions |> Map.filter(fn - {{_, fn_name, _}, _} -> Atom.to_charlist(fn_name) == char_ident + {{_, fn_name, fn_arity}, %{positions: fn_positions}} -> + Atom.to_charlist(fn_name) === definition_name and not is_nil(fn_arity) and + Enum.member?(fn_positions, {line, column}) end) |> Enum.flat_map(fn {_, %{positions: positions}} -> positions end) |> Enum.uniq() diff --git a/apps/language_server/test/providers/rename_test.exs b/apps/language_server/test/providers/rename_test.exs index 8f63d26f7..62f97f235 100644 --- a/apps/language_server/test/providers/rename_test.exs +++ b/apps/language_server/test/providers/rename_test.exs @@ -186,7 +186,7 @@ defmodule ElixirLS.LanguageServer.Providers.RenameTest do expected_edits = [ target_range, - %{line: 13, start_char: 7, end_char: 15} + %{line: 15, start_char: 7, end_char: 15} ] |> get_expected_edits("new_subtract") diff --git a/apps/language_server/test/support/fixtures/rename_example.ex b/apps/language_server/test/support/fixtures/rename_example.ex index 2110e22a3..2ee17656f 100644 --- a/apps/language_server/test/support/fixtures/rename_example.ex +++ b/apps/language_server/test/support/fixtures/rename_example.ex @@ -11,5 +11,7 @@ defmodule ElixirLS.Test.RenameExample do defp add(a, b) when is_integer(a) and is_integer(b), do: a + b defp add(a, b) when is_binary(a) and is_binary(b), do: a <> b + def add(a, b, c), do: a + b + c + defp subtract(a, b), do: a - b end From 532bf88bcc4f83366c43baa40125ea3cffb25cea Mon Sep 17 00:00:00 2001 From: Tim Gent Date: Tue, 15 Nov 2022 21:07:36 +0000 Subject: [PATCH 125/125] Simplify get_char_ident fn in rename provider --- .../lib/language_server/providers/rename.ex | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/apps/language_server/lib/language_server/providers/rename.ex b/apps/language_server/lib/language_server/providers/rename.ex index d4588009b..24f815b3a 100644 --- a/apps/language_server/lib/language_server/providers/rename.ex +++ b/apps/language_server/lib/language_server/providers/rename.ex @@ -144,10 +144,9 @@ defmodule ElixirLS.LanguageServer.Providers.Rename do end defp get_char_ident(text, line, character) do - case Code.Fragment.surround_context(text, {line, character}) do - %{context: {context, char_ident}} when context in [:local_or_var, :local_call] -> char_ident - %{context: {:dot, _, char_ident}} -> char_ident - _ -> nil + case get_begin_end_and_char_ident(text, line, character) do + nil -> nil + %{char_ident: char_ident} -> char_ident end end