-
Notifications
You must be signed in to change notification settings - Fork 205
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
Salvage code actions from experimental server #1057
Closed
Closed
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,180 @@ | ||
defmodule ElixirLS.LanguageServer.CodeUnit do | ||
@moduledoc """ | ||
Code unit and offset conversions | ||
|
||
The LSP protocol speaks in positions, which defines where something happens in a document. | ||
Positions have a start and an end, which are defined as code unit _offsets_ from the beginning | ||
of a line. this module helps to convert between utf8, which most of the world speaks | ||
natively, and utf16, which has been forced upon us by microsoft. | ||
|
||
Converting between offsets and code units is 0(n), and allocations only happen if a | ||
multi-byte character is detected, at which point, only that character is allocated. | ||
This exploits the fact that most source code consists of ascii characters, with at best, | ||
sporadic multi-byte characters in it. Thus, the vast majority of documents will not require | ||
any allocations at all. | ||
""" | ||
@type utf8_code_unit :: non_neg_integer() | ||
@type utf16_code_unit :: non_neg_integer() | ||
@type utf8_offset :: non_neg_integer() | ||
@type utf16_offset :: non_neg_integer() | ||
|
||
@type error :: {:error, :misaligned} | {:error, :out_of_bounds} | ||
|
||
# public | ||
|
||
@doc """ | ||
Converts a utf8 character offset into a utf16 character offset. This implementation | ||
clamps the maximum size of an offset so that any initial character position can be | ||
passed in and the offset returned will reflect the end of the line. | ||
""" | ||
@spec utf16_offset(String.t(), utf8_offset()) :: utf16_offset() | ||
def utf16_offset(binary, character_position) do | ||
do_utf16_offset(binary, character_position, 0) | ||
end | ||
|
||
@doc """ | ||
Converts a utf16 character offset into a utf8 character offset. This implementation | ||
clamps the maximum size of an offset so that any initial character position can be | ||
passed in and the offset returned will reflect the end of the line. | ||
""" | ||
@spec utf8_offset(String.t(), utf16_offset()) :: utf8_offset() | ||
def utf8_offset(binary, character_position) do | ||
do_utf8_offset(binary, character_position, 0) | ||
end | ||
|
||
@spec to_utf8(String.t(), utf16_code_unit()) :: {:ok, utf8_code_unit()} | error | ||
def to_utf8(binary, utf16_unit) do | ||
do_to_utf8(binary, utf16_unit, 0) | ||
end | ||
|
||
@spec to_utf16(String.t(), utf8_code_unit()) :: {:ok, utf16_code_unit()} | error | ||
def to_utf16(binary, utf16_unit) do | ||
do_to_utf16(binary, utf16_unit, 0) | ||
end | ||
|
||
def count(:utf16, binary) do | ||
do_count_utf16(binary, 0) | ||
end | ||
|
||
# Private | ||
|
||
# UTF-16 | ||
|
||
def do_count_utf16(<<>>, count) do | ||
count | ||
end | ||
|
||
def do_count_utf16(<<c, rest::binary>>, count) when c < 128 do | ||
do_count_utf16(rest, count + 1) | ||
end | ||
|
||
def do_count_utf16(<<c::utf8, rest::binary>>, count) do | ||
increment = | ||
<<c::utf16>> | ||
|> byte_size() | ||
|> div(2) | ||
|
||
do_count_utf16(rest, count + increment) | ||
end | ||
|
||
defp do_utf16_offset(_, 0, offset) do | ||
offset | ||
end | ||
|
||
defp do_utf16_offset(<<>>, _, offset) do | ||
# this clause pegs the offset at the end of the string | ||
# no matter the character index | ||
offset | ||
end | ||
|
||
defp do_utf16_offset(<<c, rest::binary>>, remaining, offset) when c < 128 do | ||
do_utf16_offset(rest, remaining - 1, offset + 1) | ||
end | ||
|
||
defp do_utf16_offset(<<c::utf8, rest::binary>>, remaining, offset) do | ||
s = <<c::utf8>> | ||
increment = utf16_size(s) | ||
do_utf16_offset(rest, remaining - 1, offset + increment) | ||
end | ||
|
||
defp do_to_utf16(_, 0, utf16_unit) do | ||
{:ok, utf16_unit} | ||
end | ||
|
||
defp do_to_utf16(_, utf8_unit, _) when utf8_unit < 0 do | ||
{:error, :misaligned} | ||
end | ||
|
||
defp do_to_utf16(<<>>, _remaining, _utf16_unit) do | ||
{:error, :out_of_bounds} | ||
end | ||
|
||
defp do_to_utf16(<<c, rest::binary>>, utf8_unit, utf16_unit) when c < 128 do | ||
do_to_utf16(rest, utf8_unit - 1, utf16_unit + 1) | ||
end | ||
|
||
defp do_to_utf16(<<c::utf8, rest::binary>>, utf8_unit, utf16_unit) do | ||
utf8_string = <<c::utf8>> | ||
increment = utf16_size(utf8_string) | ||
decrement = byte_size(utf8_string) | ||
|
||
do_to_utf16(rest, utf8_unit - decrement, utf16_unit + increment) | ||
end | ||
|
||
defp utf16_size(binary) when is_binary(binary) do | ||
binary | ||
|> :unicode.characters_to_binary(:utf8, :utf16) | ||
|> byte_size() | ||
|> div(2) | ||
end | ||
|
||
# UTF-8 | ||
|
||
defp do_utf8_offset(_, 0, offset) do | ||
offset | ||
end | ||
|
||
defp do_utf8_offset(<<>>, _, offset) do | ||
# this clause pegs the offset at the end of the string | ||
# no matter the character index | ||
offset | ||
end | ||
|
||
defp do_utf8_offset(<<c, rest::binary>>, remaining, offset) when c < 128 do | ||
do_utf8_offset(rest, remaining - 1, offset + 1) | ||
end | ||
|
||
defp do_utf8_offset(<<c::utf8, rest::binary>>, remaining, offset) do | ||
s = <<c::utf8>> | ||
increment = utf8_size(s) | ||
decrement = utf16_size(s) | ||
do_utf8_offset(rest, remaining - decrement, offset + increment) | ||
end | ||
|
||
defp do_to_utf8(_, 0, utf8_unit) do | ||
{:ok, utf8_unit} | ||
end | ||
|
||
defp do_to_utf8(_, utf_16_units, _) when utf_16_units < 0 do | ||
{:error, :misaligned} | ||
end | ||
|
||
defp do_to_utf8(<<>>, _remaining, _utf8_unit) do | ||
{:error, :out_of_bounds} | ||
end | ||
|
||
defp do_to_utf8(<<c, rest::binary>>, utf16_unit, utf8_unit) when c < 128 do | ||
do_to_utf8(rest, utf16_unit - 1, utf8_unit + 1) | ||
end | ||
|
||
defp do_to_utf8(<<c::utf8, rest::binary>>, utf16_unit, utf8_unit) do | ||
utf8_code_units = byte_size(<<c::utf8>>) | ||
utf16_code_units = utf16_size(<<c::utf8>>) | ||
|
||
do_to_utf8(rest, utf16_unit - utf16_code_units, utf8_unit + utf8_code_units) | ||
end | ||
|
||
defp utf8_size(binary) when is_binary(binary) do | ||
byte_size(binary) | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
12 changes: 12 additions & 0 deletions
12
apps/language_server/lib/language_server/providers/code_action.ex
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
defmodule ElixirLS.LanguageServer.Providers.CodeAction do | ||
alias ElixirLS.LanguageServer.Providers.CodeAction.ReplaceRemoteFunction | ||
alias ElixirLS.LanguageServer.Providers.CodeAction.ReplaceWithUnderscore | ||
|
||
@code_actions [ReplaceRemoteFunction, ReplaceWithUnderscore] | ||
|
||
def code_actions(source_file, uri, diagnostic) do | ||
code_actions = Enum.flat_map(@code_actions, & &1.apply(source_file, uri, diagnostic)) | ||
|
||
{:ok, code_actions} | ||
end | ||
end |
24 changes: 24 additions & 0 deletions
24
apps/language_server/lib/language_server/providers/code_action/code_action_result.ex
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
defmodule ElixirLS.LanguageServer.Providers.CodeAction.CodeActionResult do | ||
alias ElixirLS.LanguageServer.Protocol.TextEdit | ||
|
||
@type t :: %{ | ||
title: String.t(), | ||
kind: String.t(), | ||
edit: %{ | ||
changes: %{String.t() => TextEdit.t()} | ||
} | ||
} | ||
|
||
@spec new(String.t(), String.t(), [TextEdit.t()], String.t()) :: t() | ||
def new(title, kind, text_edits, uri) do | ||
%{ | ||
:title => title, | ||
:kind => kind, | ||
:edit => %{ | ||
:changes => %{ | ||
uri => text_edits | ||
} | ||
} | ||
} | ||
end | ||
end |
53 changes: 53 additions & 0 deletions
53
apps/language_server/lib/language_server/providers/code_action/helpers.ex
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
defmodule ElixirLS.LanguageServer.Providers.CodeAction.Helpers do | ||
alias ElixirLS.LanguageServer.Protocol.TextEdit | ||
alias ElixirLS.LanguageServer.Providers.CodeMod.Ast | ||
alias ElixirLS.LanguageServer.Providers.CodeMod.Text | ||
|
||
@spec update_line(TextEdit.t(), non_neg_integer()) :: TextEdit.t() | ||
def update_line( | ||
%TextEdit{range: %{"start" => start_line, "end" => end_line}} = text_edit, | ||
line_number | ||
) do | ||
%TextEdit{ | ||
text_edit | ||
| range: %{ | ||
"start" => %{start_line | "line" => line_number}, | ||
"end" => %{end_line | "line" => line_number} | ||
} | ||
} | ||
end | ||
|
||
@spec to_one_line_string(Ast.t()) :: {:ok, String.t()} | :error | ||
def to_one_line_string(updated_ast) do | ||
updated_ast | ||
|> Ast.to_string() | ||
# We're dealing with a single error on a single line. | ||
# If the line doesn't compile (like it has a do with no end), ElixirSense | ||
# adds additional lines to documents with errors. Also, in case of a one-line do, | ||
# ElixirSense creates do with end from the AST. | ||
|> maybe_recover_one_line_do(updated_ast) | ||
|> Text.fetch_line(0) | ||
end | ||
|
||
@do_regex ~r/\s*do\s*/ | ||
defp maybe_recover_one_line_do(updated_text, {_name, context, _children} = _updated_ast) do | ||
wrong_do_end_conditions = [ | ||
not Keyword.has_key?(context, :do), | ||
not Keyword.has_key?(context, :end), | ||
Regex.match?(@do_regex, updated_text), | ||
String.ends_with?(updated_text, "\nend") | ||
] | ||
|
||
if Enum.all?(wrong_do_end_conditions) do | ||
updated_text | ||
|> String.replace(@do_regex, ", do: ") | ||
|> String.trim_trailing("\nend") | ||
else | ||
updated_text | ||
end | ||
end | ||
|
||
defp maybe_recover_one_line_do(updated_text, _updated_ast) do | ||
updated_text | ||
end | ||
end |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the original line has one-line do, after conversion to AST and conversion back to string, the output line has
do end
. Here I made a workaround that preserves one-line do. It is possible because the ASTs of the cases differ - just Macro.to_string/1 creates the same line out of both.