From ae0f62d08780f42edf09ef2f10b9bd20dea1ea88 Mon Sep 17 00:00:00 2001 From: Aldric Giacomoni Date: Sun, 25 Mar 2018 23:25:35 -0400 Subject: [PATCH 1/4] first pass at adding custom request for macro expansion --- .../lib/language_server/protocol.ex | 12 ++ .../lib/language_server/server.ex | 176 ++++++++++++------ 2 files changed, 136 insertions(+), 52 deletions(-) diff --git a/apps/language_server/lib/language_server/protocol.ex b/apps/language_server/lib/language_server/protocol.ex index 7081e0d6..3f97f455 100644 --- a/apps/language_server/lib/language_server/protocol.ex +++ b/apps/language_server/lib/language_server/protocol.ex @@ -156,6 +156,18 @@ defmodule ElixirLS.LanguageServer.Protocol do end end + defmacro macro_expansion(id, uri, text, line) do + quote do + request(unquote(id), "elixirDocument/macroExpansion", %{ + "textDocument" => %{ + "uri" => unquote(uri), + "text" => unquote(text)}, + "position" => %{ "line" => unquote(line) } + }) + end + end + + # Other utilities defmacro range(start_line, start_character, end_line, end_character) do diff --git a/apps/language_server/lib/language_server/server.ex b/apps/language_server/lib/language_server/server.ex index 3b3311e2..c22a61d1 100644 --- a/apps/language_server/lib/language_server/server.ex +++ b/apps/language_server/lib/language_server/server.ex @@ -95,7 +95,7 @@ defmodule ElixirLS.LanguageServer.Server do end def handle_cast({:build_finished, {status, diagnostics}}, state) - when status in [:ok, :noop, :error] and is_list(diagnostics) do + when status in [:ok, :noop, :error] and is_list(diagnostics) do {:noreply, handle_build_result(status, diagnostics, state)} end @@ -175,14 +175,14 @@ defmodule ElixirLS.LanguageServer.Server do %{status: :async} = req -> error_msg = "Internal error: Request ended without result" - %{ - req - | ref: nil, - pid: nil, - status: :error, - error_type: :internal_error, - error_msg: error_msg - } + %{ + req + | ref: nil, + pid: nil, + status: :error, + error_type: :internal_error, + error_msg: error_msg + } req -> %{req | ref: nil, pid: nil} @@ -197,14 +197,14 @@ defmodule ElixirLS.LanguageServer.Server do %{status: :async} = req -> error_msg = "Internal error: " <> Exception.format_exit(reason) - %{ - req - | ref: nil, - pid: nil, - status: :error, - error_type: :internal_error, - error_msg: error_msg - } + %{ + req + | ref: nil, + pid: nil, + status: :error, + error_type: :internal_error, + error_msg: error_msg + } req -> %{req | ref: nil, pid: nil} @@ -243,7 +243,7 @@ defmodule ElixirLS.LanguageServer.Server do update_request(state, id, fn %{status: :async, pid: pid} = req -> Process.exit(pid, :kill) - %{req | pid: nil, ref: nil, status: :error, error_type: :request_cancelled} + %{req | pid: nil, ref: nil, status: :error, error_type: :request_cancelled} req -> req @@ -294,7 +294,7 @@ defmodule ElixirLS.LanguageServer.Server do needs_build = Enum.any?(changes, fn %{"uri" => uri, "type" => type} -> Path.extname(uri) in [".ex", ".exs", ".erl", ".yrl", ".xrl", ".eex"] and - (type in [1, 3] or not Map.has_key?(state.source_files, uri)) + (type in [1, 3] or not Map.has_key?(state.source_files, uri)) end) if needs_build, do: trigger_build(state), else: state @@ -372,11 +372,11 @@ defmodule ElixirLS.LanguageServer.Server do defp handle_request(completion_req(_id, uri, line, character), state) do snippets_supported = !!get_in(state.client_capabilities, [ - "textDocument", - "completion", - "completionItem", - "snippetSupport" - ]) + "textDocument", + "completion", + "completionItem", + "snippetSupport" + ]) fun = fn -> Completion.completion(state.source_files[uri].text, line, character, snippets_supported) @@ -403,6 +403,15 @@ defmodule ElixirLS.LanguageServer.Server do {:async, fun, state} end + defp handle_request(macro_expansion(_id, uri, text, line) = x, state) do + y = string_to_ast(text, true, line) + x = ElixirSense.expand_full(text, y, line) + IO.inspect y + IO.inspect "--------" + IO.inspect x + {:ok, x, state} + end + defp handle_request(request(_, _, _) = req, state) do IO.inspect(req, label: "Unmatched request") {:error, :invalid_request, nil, state} @@ -434,6 +443,7 @@ defmodule ElixirLS.LanguageServer.Server do defp server_capabilities do %{ + "macroExpansion" => true, "textDocumentSync" => 2, "hoverProvider" => true, "completionProvider" => %{"triggerCharacters" => Completion.trigger_characters()}, @@ -473,8 +483,8 @@ defmodule ElixirLS.LanguageServer.Server do defp dialyze(state) do warn_opts = - (state.settings["dialyzerWarnOpts"] || []) - |> Enum.map(&String.to_atom/1) + (state.settings["dialyzerWarnOpts"] || []) + |> Enum.map(&String.to_atom/1) if dialyzer_enabled?(state), do: Dialyzer.analyze(warn_opts) state @@ -486,15 +496,15 @@ defmodule ElixirLS.LanguageServer.Server do state = cond do - state.needs_build? -> - state + state.needs_build? -> + state - status == :error or not dialyzer_enabled?(state) -> - put_in(state.dialyzer_diagnostics, []) + status == :error or not dialyzer_enabled?(state) -> + put_in(state.dialyzer_diagnostics, []) - true -> - dialyze(state) - end + true -> + dialyze(state) + end publish_diagnostics( state.build_diagnostics ++ state.dialyzer_diagnostics, @@ -531,8 +541,8 @@ 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), - do: Build.publish_file_diagnostics(uri, new_diagnostics, Map.get(source_files, uri)) + uri = SourceFile.path_to_uri(file), + do: Build.publish_file_diagnostics(uri, new_diagnostics, Map.get(source_files, uri)) end defp show_version_warnings do @@ -548,20 +558,20 @@ defmodule ElixirLS.LanguageServer.Server do warning = cond do - otp_version < 19 -> - "Upgrade Erlang to version OTP 20 for debugging support and automatic, " <> - "incremental Dialyzer integration." + otp_version < 19 -> + "Upgrade Erlang to version OTP 20 for debugging support and automatic, " <> + "incremental Dialyzer integration." - otp_version < 20 -> - "Upgrade Erlang to version OTP 20 for automatic, incremental Dialyzer integration." + otp_version < 20 -> + "Upgrade Erlang to version OTP 20 for automatic, incremental Dialyzer integration." - otp_version > 20 -> - "ElixirLS Dialyzer integration has not been tested with Erlang versions other than " <> - "OTP 20. To disable, set \"elixirLS.enableDialyzer\" to false." + otp_version > 20 -> + "ElixirLS Dialyzer integration has not been tested with Erlang versions other than " <> + "OTP 20. To disable, set \"elixirLS.enableDialyzer\" to false." - true -> - nil - end + true -> + nil + end if warning != nil, do: JsonRpc.show_message(:info, warning <> " (Currently OTP #{otp_version})") @@ -609,15 +619,15 @@ defmodule ElixirLS.LanguageServer.Server do end defp set_project_dir(%{project_dir: prev_project_dir, root_uri: root_uri} = state, project_dir) - when is_binary(root_uri) do + when is_binary(root_uri) do root_dir = SourceFile.path_from_uri(root_uri) project_dir = - if is_binary(project_dir) do - Path.absname(Path.join(root_dir, project_dir)) - else - root_dir - end + if is_binary(project_dir) do + Path.absname(Path.join(root_dir, project_dir)) + else + root_dir + end cond do not File.dir?(project_dir) -> @@ -644,4 +654,66 @@ defmodule ElixirLS.LanguageServer.Server do defp set_project_dir(state, _) do state end + + + defp string_to_ast(source, try_to_fix_parse_error, cursor_line_number) do + case Code.string_to_quoted(source) do + {:ok, ast} -> + {:ok, ast} + error -> + # IO.puts :stderr, "PARSE ERROR" + # IO.inspect :stderr, error, [] + if try_to_fix_parse_error do + source + |> fix_parse_error(cursor_line_number, error) + |> string_to_ast(false, cursor_line_number) + else + error + end + end + end + + defp fix_parse_error(source, _cursor_line_number, {:error, {line, {"\"" <> <<_::bytes-size(1)>> <> "\" is missing terminator" <> _, _}, _}}) when is_integer(line) do + source + |> replace_line_with_marker(line) + end + + defp fix_parse_error(source, _cursor_line_number, {:error, {_line, {_error_type, text}, _token}}) do + [_, line] = Regex.run(Regex.recompile!(~r/line\s(\d+)/), text) + line = line |> String.to_integer + source + |> replace_line_with_marker(line) + end + + defp fix_parse_error(source, cursor_line_number, {:error, {line, "syntax" <> _, "'end'"}}) when is_integer(line) do + source + |> replace_line_with_marker(cursor_line_number) + end + + defp fix_parse_error(source, _cursor_line_number, {:error, {line, "syntax" <> _, _token}}) when is_integer(line) do + source + |> replace_line_with_marker(line) + end + + defp fix_parse_error(_, nil, error) do + error + end + + defp fix_parse_error(source, cursor_line_number, _error) do + source + |> replace_line_with_marker(cursor_line_number) + end + + defp fix_line_not_found(source, line_number) do + source |> replace_line_with_marker(line_number) + end + + defp replace_line_with_marker(source, line) do + # IO.puts :stderr, "REPLACING LINE: #{line}" + source + |> String.split(["\n", "\r\n"]) + |> List.replace_at(line - 1, "(__atom_elixir_marker_#{line}__())") + |> Enum.join("\n") + end + end From 20e1244189352161ccd31519eb08f5a7aa3f473f Mon Sep 17 00:00:00 2001 From: Aldric Giacomoni Date: Sun, 25 Mar 2018 23:31:25 -0400 Subject: [PATCH 2/4] let server format its own buffer --- .../lib/language_server/server.ex | 153 ++++++++++-------- 1 file changed, 82 insertions(+), 71 deletions(-) diff --git a/apps/language_server/lib/language_server/server.ex b/apps/language_server/lib/language_server/server.ex index c22a61d1..9fa24562 100644 --- a/apps/language_server/lib/language_server/server.ex +++ b/apps/language_server/lib/language_server/server.ex @@ -95,7 +95,7 @@ defmodule ElixirLS.LanguageServer.Server do end def handle_cast({:build_finished, {status, diagnostics}}, state) - when status in [:ok, :noop, :error] and is_list(diagnostics) do + when status in [:ok, :noop, :error] and is_list(diagnostics) do {:noreply, handle_build_result(status, diagnostics, state)} end @@ -175,14 +175,14 @@ defmodule ElixirLS.LanguageServer.Server do %{status: :async} = req -> error_msg = "Internal error: Request ended without result" - %{ - req - | ref: nil, - pid: nil, - status: :error, - error_type: :internal_error, - error_msg: error_msg - } + %{ + req + | ref: nil, + pid: nil, + status: :error, + error_type: :internal_error, + error_msg: error_msg + } req -> %{req | ref: nil, pid: nil} @@ -197,14 +197,14 @@ defmodule ElixirLS.LanguageServer.Server do %{status: :async} = req -> error_msg = "Internal error: " <> Exception.format_exit(reason) - %{ - req - | ref: nil, - pid: nil, - status: :error, - error_type: :internal_error, - error_msg: error_msg - } + %{ + req + | ref: nil, + pid: nil, + status: :error, + error_type: :internal_error, + error_msg: error_msg + } req -> %{req | ref: nil, pid: nil} @@ -243,7 +243,7 @@ defmodule ElixirLS.LanguageServer.Server do update_request(state, id, fn %{status: :async, pid: pid} = req -> Process.exit(pid, :kill) - %{req | pid: nil, ref: nil, status: :error, error_type: :request_cancelled} + %{req | pid: nil, ref: nil, status: :error, error_type: :request_cancelled} req -> req @@ -294,7 +294,7 @@ defmodule ElixirLS.LanguageServer.Server do needs_build = Enum.any?(changes, fn %{"uri" => uri, "type" => type} -> Path.extname(uri) in [".ex", ".exs", ".erl", ".yrl", ".xrl", ".eex"] and - (type in [1, 3] or not Map.has_key?(state.source_files, uri)) + (type in [1, 3] or not Map.has_key?(state.source_files, uri)) end) if needs_build, do: trigger_build(state), else: state @@ -372,11 +372,11 @@ defmodule ElixirLS.LanguageServer.Server do defp handle_request(completion_req(_id, uri, line, character), state) do snippets_supported = !!get_in(state.client_capabilities, [ - "textDocument", - "completion", - "completionItem", - "snippetSupport" - ]) + "textDocument", + "completion", + "completionItem", + "snippetSupport" + ]) fun = fn -> Completion.completion(state.source_files[uri].text, line, character, snippets_supported) @@ -406,9 +406,9 @@ defmodule ElixirLS.LanguageServer.Server do defp handle_request(macro_expansion(_id, uri, text, line) = x, state) do y = string_to_ast(text, true, line) x = ElixirSense.expand_full(text, y, line) - IO.inspect y - IO.inspect "--------" - IO.inspect x + IO.inspect(y) + IO.inspect("--------") + IO.inspect(x) {:ok, x, state} end @@ -483,8 +483,8 @@ defmodule ElixirLS.LanguageServer.Server do defp dialyze(state) do warn_opts = - (state.settings["dialyzerWarnOpts"] || []) - |> Enum.map(&String.to_atom/1) + (state.settings["dialyzerWarnOpts"] || []) + |> Enum.map(&String.to_atom/1) if dialyzer_enabled?(state), do: Dialyzer.analyze(warn_opts) state @@ -496,15 +496,15 @@ defmodule ElixirLS.LanguageServer.Server do state = cond do - state.needs_build? -> - state + state.needs_build? -> + state - status == :error or not dialyzer_enabled?(state) -> - put_in(state.dialyzer_diagnostics, []) + status == :error or not dialyzer_enabled?(state) -> + put_in(state.dialyzer_diagnostics, []) - true -> - dialyze(state) - end + true -> + dialyze(state) + end publish_diagnostics( state.build_diagnostics ++ state.dialyzer_diagnostics, @@ -541,8 +541,8 @@ 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), - do: Build.publish_file_diagnostics(uri, new_diagnostics, Map.get(source_files, uri)) + uri = SourceFile.path_to_uri(file), + do: Build.publish_file_diagnostics(uri, new_diagnostics, Map.get(source_files, uri)) end defp show_version_warnings do @@ -558,20 +558,20 @@ defmodule ElixirLS.LanguageServer.Server do warning = cond do - otp_version < 19 -> - "Upgrade Erlang to version OTP 20 for debugging support and automatic, " <> - "incremental Dialyzer integration." + otp_version < 19 -> + "Upgrade Erlang to version OTP 20 for debugging support and automatic, " <> + "incremental Dialyzer integration." - otp_version < 20 -> - "Upgrade Erlang to version OTP 20 for automatic, incremental Dialyzer integration." + otp_version < 20 -> + "Upgrade Erlang to version OTP 20 for automatic, incremental Dialyzer integration." - otp_version > 20 -> - "ElixirLS Dialyzer integration has not been tested with Erlang versions other than " <> - "OTP 20. To disable, set \"elixirLS.enableDialyzer\" to false." + otp_version > 20 -> + "ElixirLS Dialyzer integration has not been tested with Erlang versions other than " <> + "OTP 20. To disable, set \"elixirLS.enableDialyzer\" to false." - true -> - nil - end + true -> + nil + end if warning != nil, do: JsonRpc.show_message(:info, warning <> " (Currently OTP #{otp_version})") @@ -619,15 +619,15 @@ defmodule ElixirLS.LanguageServer.Server do end defp set_project_dir(%{project_dir: prev_project_dir, root_uri: root_uri} = state, project_dir) - when is_binary(root_uri) do + when is_binary(root_uri) do root_dir = SourceFile.path_from_uri(root_uri) project_dir = - if is_binary(project_dir) do - Path.absname(Path.join(root_dir, project_dir)) - else - root_dir - end + if is_binary(project_dir) do + Path.absname(Path.join(root_dir, project_dir)) + else + root_dir + end cond do not File.dir?(project_dir) -> @@ -655,42 +655,54 @@ defmodule ElixirLS.LanguageServer.Server do state end - defp string_to_ast(source, try_to_fix_parse_error, cursor_line_number) do case Code.string_to_quoted(source) do {:ok, ast} -> {:ok, ast} + error -> - # IO.puts :stderr, "PARSE ERROR" - # IO.inspect :stderr, error, [] - if try_to_fix_parse_error do - source - |> fix_parse_error(cursor_line_number, error) - |> string_to_ast(false, cursor_line_number) - else - error - end + # IO.puts :stderr, "PARSE ERROR" + # IO.inspect :stderr, error, [] + if try_to_fix_parse_error do + source + |> fix_parse_error(cursor_line_number, error) + |> string_to_ast(false, cursor_line_number) + else + error + end end end - defp fix_parse_error(source, _cursor_line_number, {:error, {line, {"\"" <> <<_::bytes-size(1)>> <> "\" is missing terminator" <> _, _}, _}}) when is_integer(line) do + defp fix_parse_error( + source, + _cursor_line_number, + {:error, {line, {"\"" <> <<_::bytes-size(1)>> <> "\" is missing terminator" <> _, _}, _}} + ) + when is_integer(line) do source |> replace_line_with_marker(line) end - defp fix_parse_error(source, _cursor_line_number, {:error, {_line, {_error_type, text}, _token}}) do + defp fix_parse_error( + source, + _cursor_line_number, + {:error, {_line, {_error_type, text}, _token}} + ) do [_, line] = Regex.run(Regex.recompile!(~r/line\s(\d+)/), text) - line = line |> String.to_integer + line = line |> String.to_integer() + source |> replace_line_with_marker(line) end - defp fix_parse_error(source, cursor_line_number, {:error, {line, "syntax" <> _, "'end'"}}) when is_integer(line) do + defp fix_parse_error(source, cursor_line_number, {:error, {line, "syntax" <> _, "'end'"}}) + when is_integer(line) do source |> replace_line_with_marker(cursor_line_number) end - defp fix_parse_error(source, _cursor_line_number, {:error, {line, "syntax" <> _, _token}}) when is_integer(line) do + defp fix_parse_error(source, _cursor_line_number, {:error, {line, "syntax" <> _, _token}}) + when is_integer(line) do source |> replace_line_with_marker(line) end @@ -715,5 +727,4 @@ defmodule ElixirLS.LanguageServer.Server do |> List.replace_at(line - 1, "(__atom_elixir_marker_#{line}__())") |> Enum.join("\n") end - end From 9f2e2e3f9db8d5b4aa7d1432d4fc1bb95903490a Mon Sep 17 00:00:00 2001 From: Aldric Giacomoni Date: Sat, 31 Mar 2018 19:53:33 -0400 Subject: [PATCH 3/4] define API for communication with elixir_sense to expand macros --- apps/language_server/lib/language_server/protocol.ex | 8 ++++---- apps/language_server/lib/language_server/server.ex | 8 ++------ 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/apps/language_server/lib/language_server/protocol.ex b/apps/language_server/lib/language_server/protocol.ex index 3f97f455..8141140b 100644 --- a/apps/language_server/lib/language_server/protocol.ex +++ b/apps/language_server/lib/language_server/protocol.ex @@ -156,13 +156,13 @@ defmodule ElixirLS.LanguageServer.Protocol do end end - defmacro macro_expansion(id, uri, text, line) do + defmacro macro_expansion(id, whole_buffer, selected_macro, macro_line) do quote do request(unquote(id), "elixirDocument/macroExpansion", %{ + "context" => %{"selection" => unquote(selected_macro)}, "textDocument" => %{ - "uri" => unquote(uri), - "text" => unquote(text)}, - "position" => %{ "line" => unquote(line) } + "text" => unquote(whole_buffer)}, + "position" => %{ "line" => unquote(macro_line) } }) end end diff --git a/apps/language_server/lib/language_server/server.ex b/apps/language_server/lib/language_server/server.ex index 9fa24562..aa00dd62 100644 --- a/apps/language_server/lib/language_server/server.ex +++ b/apps/language_server/lib/language_server/server.ex @@ -403,12 +403,8 @@ defmodule ElixirLS.LanguageServer.Server do {:async, fun, state} end - defp handle_request(macro_expansion(_id, uri, text, line) = x, state) do - y = string_to_ast(text, true, line) - x = ElixirSense.expand_full(text, y, line) - IO.inspect(y) - IO.inspect("--------") - IO.inspect(x) + defp handle_request(macro_expansion(_id, whole_buffer, selected_macro, macro_line), state) do + x = ElixirSense.expand_full(whole_buffer, selected_macro, macro_line) {:ok, x, state} end From 41e891b8434eb19538fdb37c490d933003fd9db9 Mon Sep 17 00:00:00 2001 From: Aldric Giacomoni Date: Sat, 31 Mar 2018 20:39:45 -0400 Subject: [PATCH 4/4] remove code I don't need --- .../lib/language_server/server.ex | 72 ------------------- 1 file changed, 72 deletions(-) diff --git a/apps/language_server/lib/language_server/server.ex b/apps/language_server/lib/language_server/server.ex index aa00dd62..7419f86a 100644 --- a/apps/language_server/lib/language_server/server.ex +++ b/apps/language_server/lib/language_server/server.ex @@ -651,76 +651,4 @@ defmodule ElixirLS.LanguageServer.Server do state end - defp string_to_ast(source, try_to_fix_parse_error, cursor_line_number) do - case Code.string_to_quoted(source) do - {:ok, ast} -> - {:ok, ast} - - error -> - # IO.puts :stderr, "PARSE ERROR" - # IO.inspect :stderr, error, [] - if try_to_fix_parse_error do - source - |> fix_parse_error(cursor_line_number, error) - |> string_to_ast(false, cursor_line_number) - else - error - end - end - end - - defp fix_parse_error( - source, - _cursor_line_number, - {:error, {line, {"\"" <> <<_::bytes-size(1)>> <> "\" is missing terminator" <> _, _}, _}} - ) - when is_integer(line) do - source - |> replace_line_with_marker(line) - end - - defp fix_parse_error( - source, - _cursor_line_number, - {:error, {_line, {_error_type, text}, _token}} - ) do - [_, line] = Regex.run(Regex.recompile!(~r/line\s(\d+)/), text) - line = line |> String.to_integer() - - source - |> replace_line_with_marker(line) - end - - defp fix_parse_error(source, cursor_line_number, {:error, {line, "syntax" <> _, "'end'"}}) - when is_integer(line) do - source - |> replace_line_with_marker(cursor_line_number) - end - - defp fix_parse_error(source, _cursor_line_number, {:error, {line, "syntax" <> _, _token}}) - when is_integer(line) do - source - |> replace_line_with_marker(line) - end - - defp fix_parse_error(_, nil, error) do - error - end - - defp fix_parse_error(source, cursor_line_number, _error) do - source - |> replace_line_with_marker(cursor_line_number) - end - - defp fix_line_not_found(source, line_number) do - source |> replace_line_with_marker(line_number) - end - - defp replace_line_with_marker(source, line) do - # IO.puts :stderr, "REPLACING LINE: #{line}" - source - |> String.split(["\n", "\r\n"]) - |> List.replace_at(line - 1, "(__atom_elixir_marker_#{line}__())") - |> Enum.join("\n") - end end