Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for completions in debugger #679

Merged
merged 3 commits into from
Feb 21, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions apps/elixir_ls_debugger/lib/debugger/completions.ex
Original file line number Diff line number Diff line change
@@ -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
6 changes: 6 additions & 0 deletions apps/elixir_ls_debugger/lib/debugger/protocol.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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)})
Expand Down
57 changes: 55 additions & 2 deletions apps/elixir_ls_debugger/lib/debugger/server.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -942,7 +994,8 @@ defmodule ElixirLS.Debugger.Server do
"supportsRestartFrame" => false,
"supportsGotoTargetsRequest" => false,
"supportsStepInTargetsRequest" => false,
"supportsCompletionsRequest" => false,
"supportsCompletionsRequest" => true,
"completionTriggerCharacters" => [".", "&", "%", "^", ":", "!", "-", "~"],
"supportsModulesRequest" => false,
"additionalModuleColumns" => [],
"supportedChecksumAlgorithms" => [],
Expand Down
31 changes: 31 additions & 0 deletions apps/elixir_ls_debugger/lib/debugger/utils.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
24 changes: 24 additions & 0 deletions apps/elixir_ls_debugger/test/debugger_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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
18 changes: 18 additions & 0 deletions apps/elixir_ls_debugger/test/utils_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions apps/language_server/lib/language_server/source_file.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
22 changes: 11 additions & 11 deletions apps/language_server/test/source_file_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion mix.lock
Original file line number Diff line number Diff line change
@@ -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"},
Expand Down