Skip to content

Commit ba6252f

Browse files
authored
Add support for completions in debugger (#679)
* fix typo * warn about not supported client options * Add support for debugger completions
1 parent a0e60f1 commit ba6252f

File tree

14 files changed

+209
-21
lines changed

14 files changed

+209
-21
lines changed
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
defmodule ElixirLS.Debugger.Completions do
2+
# type CompletionItemType = 'method' | 'function' | 'constructor' | 'field'
3+
# | 'variable' | 'class' | 'interface' | 'module' | 'property' | 'unit'
4+
# | 'value' | 'enum' | 'keyword' | 'snippet' | 'text' | 'color' | 'file'
5+
# | 'reference' | 'customcolor';
6+
def map(%{
7+
type: type,
8+
name: name,
9+
arity: arity,
10+
snippet: snippet
11+
})
12+
when type in [:function, :macro] do
13+
%{
14+
type: "function",
15+
label: "#{name}/#{arity}",
16+
text: snippet || name
17+
}
18+
end
19+
20+
def map(%{
21+
type: :module,
22+
name: name
23+
}) do
24+
text =
25+
case name do
26+
":" <> rest -> rest
27+
other -> other
28+
end
29+
30+
%{
31+
type: "module",
32+
label: name,
33+
text: text
34+
}
35+
end
36+
37+
def map(%{
38+
type: :variable,
39+
name: name
40+
}) do
41+
%{
42+
type: "variable",
43+
label: name
44+
}
45+
end
46+
47+
def map(%{
48+
type: :field,
49+
name: name
50+
}) do
51+
%{
52+
type: "field",
53+
label: name
54+
}
55+
end
56+
end

apps/elixir_ls_debugger/lib/debugger/protocol.ex

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,12 @@ defmodule ElixirLS.Debugger.Protocol do
8585
end
8686
end
8787

88+
defmacro completions_req(seq, text) do
89+
quote do
90+
request(unquote(seq), "completions", %{"text" => unquote(text)})
91+
end
92+
end
93+
8894
defmacro continue_req(seq, thread_id) do
8995
quote do
9096
request(unquote(seq), "continue", %{"threadId" => unquote(thread_id)})

apps/elixir_ls_debugger/lib/debugger/server.ex

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,21 @@ defmodule ElixirLS.Debugger.Server do
228228
## Helpers
229229

230230
defp handle_request(initialize_req(_, client_info), %__MODULE__{client_info: nil} = state) do
231+
# linesStartAt1 is true by default and we only support 1-based indexing
232+
if client_info["linesStartAt1"] == false do
233+
IO.warn("0-based lines are not supported")
234+
end
235+
236+
# columnsStartAt1 is true by default and we only support 1-based indexing
237+
if client_info["columnsStartAt1"] == false do
238+
IO.warn("0-based columns are not supported")
239+
end
240+
241+
# pathFormat is `path` by default and we do not support other, e.g. `uri`
242+
if client_info["pathFormat"] not in [nil, "path"] do
243+
IO.warn("pathFormat #{client_info["pathFormat"]} not supported")
244+
end
245+
231246
{capabilities(), %{state | client_info: client_info}}
232247
end
233248

@@ -240,7 +255,11 @@ defmodule ElixirLS.Debugger.Server do
240255
}
241256
end
242257

243-
defp handle_request(launch_req(_, config), state = %__MODULE__{}) do
258+
defp handle_request(launch_req(_, config) = args, state = %__MODULE__{}) do
259+
if args["arguments"]["noDebug"] == true do
260+
IO.warn("launch with no debug is not supported")
261+
end
262+
244263
{_, ref} = spawn_monitor(fn -> initialize(config) end)
245264

246265
receive do
@@ -629,6 +648,39 @@ defmodule ElixirLS.Debugger.Server do
629648
%{state | paused_processes: maybe_continue_other_processes(args, paused_processes, pid)}}
630649
end
631650

651+
defp handle_request(completions_req(_, text) = args, state = %__MODULE__{}) do
652+
# assume that the position is 1-based
653+
line = (args["arguments"]["line"] || 1) - 1
654+
column = (args["arguments"]["column"] || 1) - 1
655+
656+
# for simplicity take only text from the given line up to column
657+
line =
658+
text
659+
|> String.split(["\r\n", "\n", "\r"])
660+
|> Enum.at(line)
661+
662+
# it's not documented but VSCode uses utf16 positions
663+
column = Utils.dap_character_to_elixir(line, column)
664+
prefix = String.slice(line, 0, column)
665+
666+
vars =
667+
all_variables(state.paused_processes, args["arguments"]["frameId"])
668+
|> Enum.map(fn {name, value} ->
669+
%ElixirSense.Core.State.VarInfo{
670+
name: name,
671+
type: ElixirSense.Core.Binding.from_var(value)
672+
}
673+
end)
674+
675+
env = %ElixirSense.Providers.Suggestion.Complete.Env{vars: vars}
676+
677+
results =
678+
ElixirSense.Providers.Suggestion.Complete.complete(prefix, env)
679+
|> Enum.map(&ElixirLS.Debugger.Completions.map/1)
680+
681+
{%{"targets" => results}, state}
682+
end
683+
632684
defp handle_request(request(_, command), _state = %__MODULE__{}) when is_binary(command) do
633685
raise ServerError,
634686
message: "notSupported",
@@ -961,7 +1013,8 @@ defmodule ElixirLS.Debugger.Server do
9611013
"supportsRestartFrame" => false,
9621014
"supportsGotoTargetsRequest" => false,
9631015
"supportsStepInTargetsRequest" => false,
964-
"supportsCompletionsRequest" => false,
1016+
"supportsCompletionsRequest" => true,
1017+
"completionTriggerCharacters" => [".", "&", "%", "^", ":", "!", "-", "~"],
9651018
"supportsModulesRequest" => false,
9661019
"additionalModuleColumns" => [],
9671020
"supportedChecksumAlgorithms" => [],

apps/elixir_ls_debugger/lib/debugger/utils.ex

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,35 @@ defmodule ElixirLS.Debugger.Utils do
1818
{:error, "cannot parse MFA"}
1919
end
2020
end
21+
22+
defp characters_to_binary!(binary, from, to) do
23+
case :unicode.characters_to_binary(binary, from, to) do
24+
result when is_binary(result) -> result
25+
end
26+
end
27+
28+
def dap_character_to_elixir(_utf8_line, dap_character) when dap_character <= 0, do: 0
29+
30+
def dap_character_to_elixir(utf8_line, dap_character) do
31+
utf16_line =
32+
utf8_line
33+
|> characters_to_binary!(:utf8, :utf16)
34+
35+
byte_size = byte_size(utf16_line)
36+
37+
# if character index is over the length of the string assume we pad it with spaces (1 byte in utf8)
38+
diff = div(max(dap_character * 2 - byte_size, 0), 2)
39+
40+
utf8_character =
41+
utf16_line
42+
|> (&binary_part(
43+
&1,
44+
0,
45+
min(dap_character * 2, byte_size)
46+
)).()
47+
|> characters_to_binary!(:utf16, :utf8)
48+
|> String.length()
49+
50+
utf8_character + diff
51+
end
2152
end

apps/elixir_ls_debugger/test/debugger_test.exs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1545,4 +1545,28 @@ defmodule ElixirLS.Debugger.ServerTest do
15451545
assert Process.alive?(server)
15461546
end
15471547
end
1548+
1549+
test "Completions", %{server: server} do
1550+
Server.receive_packet(server, initialize_req(1, %{}))
1551+
assert_receive(response(_, 1, "initialize", _))
1552+
1553+
Server.receive_packet(
1554+
server,
1555+
%{
1556+
"arguments" => %{
1557+
"text" => "DateTi",
1558+
"column" => 7
1559+
},
1560+
"command" => "completions",
1561+
"seq" => 1,
1562+
"type" => "request"
1563+
}
1564+
)
1565+
1566+
assert_receive(%{"body" => %{"targets" => _targets}}, 10000)
1567+
1568+
assert Process.alive?(server)
1569+
1570+
# assert [%{}]
1571+
end
15481572
end

apps/elixir_ls_debugger/test/utils_test.exs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,22 @@ defmodule ElixirLS.Debugger.UtilsTest do
3333
assert {:error, "cannot parse MFA"} == Utils.parse_mfa("")
3434
end
3535
end
36+
37+
describe "positions" do
38+
test "dap_character_to_elixir empty" do
39+
assert 0 == Utils.dap_character_to_elixir("", 0)
40+
end
41+
42+
test "dap_character_to_elixir first char" do
43+
assert 0 == Utils.dap_character_to_elixir("abcde", 0)
44+
end
45+
46+
test "dap_character_to_elixir line" do
47+
assert 1 == Utils.dap_character_to_elixir("abcde", 1)
48+
end
49+
50+
test "dap_character_to_elixir utf8" do
51+
assert 1 == Utils.dap_character_to_elixir("🏳️‍🌈abcde", 6)
52+
end
53+
end
3654
end

apps/language_server/lib/language_server/providers/definition.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ defmodule ElixirLS.LanguageServer.Providers.Definition do
66
alias ElixirLS.LanguageServer.{Protocol, SourceFile}
77

88
def definition(uri, text, line, character) do
9-
{line, character} = SourceFile.lsp_position_to_elixr(text, {line, character})
9+
{line, character} = SourceFile.lsp_position_to_elixir(text, {line, character})
1010

1111
result =
1212
case ElixirSense.definition(text, line, character) do

apps/language_server/lib/language_server/providers/hover.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ defmodule ElixirLS.LanguageServer.Providers.Hover do
1818
|> Enum.map(fn x -> "lib/#{x}/lib" end)
1919

2020
def hover(text, line, character, project_dir) do
21-
{line, character} = SourceFile.lsp_position_to_elixr(text, {line, character})
21+
{line, character} = SourceFile.lsp_position_to_elixir(text, {line, character})
2222

2323
response =
2424
case ElixirSense.docs(text, line, character) do

apps/language_server/lib/language_server/providers/implementation.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ defmodule ElixirLS.LanguageServer.Providers.Implementation do
66
alias ElixirLS.LanguageServer.{Protocol, SourceFile}
77

88
def implementation(uri, text, line, character) do
9-
{line, character} = SourceFile.lsp_position_to_elixr(text, {line, character})
9+
{line, character} = SourceFile.lsp_position_to_elixir(text, {line, character})
1010
locations = ElixirSense.implementations(text, line, character)
1111
results = for location <- locations, do: Protocol.Location.new(location, uri, text)
1212

apps/language_server/lib/language_server/providers/references.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ defmodule ElixirLS.LanguageServer.Providers.References do
1414
import ElixirLS.LanguageServer.Protocol
1515

1616
def references(text, uri, line, character, _include_declaration) do
17-
{line, character} = SourceFile.lsp_position_to_elixr(text, {line, character})
17+
{line, character} = SourceFile.lsp_position_to_elixir(text, {line, character})
1818

1919
Build.with_build_lock(fn ->
2020
ElixirSense.references(text, line, character)

0 commit comments

Comments
 (0)