Skip to content

Commit

Permalink
salvage code actions from experimental server
Browse files Browse the repository at this point in the history
  • Loading branch information
sheldak committed Jan 25, 2024
1 parent d10ce2c commit 2277649
Show file tree
Hide file tree
Showing 18 changed files with 1,718 additions and 3 deletions.
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)
|> 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

0 comments on commit 2277649

Please sign in to comment.