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

Salvage code actions from experimental server #1057

Closed
Closed
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
180 changes: 180 additions & 0 deletions apps/language_server/lib/language_server/code_unit.ex
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
11 changes: 11 additions & 0 deletions apps/language_server/lib/language_server/protocol.ex
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,17 @@ defmodule ElixirLS.LanguageServer.Protocol do
end
end

defmacro code_action_req(id, uri, diagnostics) do
quote do
request(unquote(id), "textDocument/codeAction", %{
"context" => %{"diagnostics" => unquote(diagnostics)},
"textDocument" => %{
"uri" => unquote(uri)
}
})
end
end

# Other utilities

defmacro range(start_line, start_character, end_line, end_character) do
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ defmodule ElixirLS.LanguageServer.Protocol.TextEdit do

For details see https://microsoft.github.io/language-server-protocol/specification#textEdit
"""

@type t :: %__MODULE__{}

@derive JasonV.Encoder
defstruct [:range, :newText]
end
12 changes: 12 additions & 0 deletions apps/language_server/lib/language_server/providers/code_action.ex
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
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
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)
Comment on lines +24 to +28
Copy link
Contributor Author

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.

|> 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
Loading
Loading