Skip to content

Commit

Permalink
Add support for completions in debugger (#679)
Browse files Browse the repository at this point in the history
* fix typo

* warn about not supported client options

* Add support for debugger completions
  • Loading branch information
lukaszsamson committed Feb 21, 2022
1 parent a0e60f1 commit ba6252f
Show file tree
Hide file tree
Showing 14 changed files with 209 additions and 21 deletions.
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 @@ -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",
Expand Down Expand Up @@ -961,7 +1013,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

0 comments on commit ba6252f

Please sign in to comment.