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 acae675f6..9cad8e76a 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 @@ -610,6 +629,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", @@ -942,7 +994,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"},