From db0e07fb28e811c08353e5c0068692817f3ca6eb Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Tue, 6 Feb 2024 23:58:30 +0100 Subject: [PATCH] more cases covered --- .../lib/language_server/ast_utils.ex | 438 ++++++++-------- .../providers/selection_ranges.ex | 462 +++++------------ .../lib/language_server/range_utils.ex | 4 +- apps/language_server/test/ast_utils_test.exs | 53 +- .../test/providers/selection_ranges_test.exs | 467 +++++++++++------- 5 files changed, 717 insertions(+), 707 deletions(-) diff --git a/apps/language_server/lib/language_server/ast_utils.ex b/apps/language_server/lib/language_server/ast_utils.ex index 2a09f77fa..0f86039a4 100644 --- a/apps/language_server/lib/language_server/ast_utils.ex +++ b/apps/language_server/lib/language_server/ast_utils.ex @@ -6,20 +6,26 @@ defmodule ElixirLS.LanguageServer.AstUtils do @unary_operators ~w[@ + - ! ^ not &]a def node_range(atom) when is_atom(atom), do: nil + def node_range([{{:__block__, _, [atom]} = first, _} | _] = list) when is_atom(atom) do case List.last(list) do {_, last} -> case {node_range(first), node_range(last)} do {range(start_line, start_character, _, _), range(_, _, end_line, end_character)} -> range(start_line, start_character, end_line, end_character) - _ -> nil - end - _ -> nil + + _ -> + nil + end + + _ -> + nil end end + def node_range(list) when is_list(list), do: nil - def node_range({:__block__, meta, args} = ast) do + def node_range({:__block__, meta, args} = _ast) do line = Keyword.get(meta, :line) column = Keyword.get(meta, :column) @@ -31,70 +37,75 @@ defmodule ElixirLS.LanguageServer.AstUtils do case {node_range(first), node_range(last)} do {range(start_line, start_character, _, _), range(_, _, end_line, end_character)} -> range(start_line, start_character, end_line, end_character) - _ -> nil + + _ -> + nil end end else line = line - 1 column = column - 1 - {end_line, end_column} = cond do - token = meta[:token] -> - {line, column + String.length(token)} - end_location = meta[:closing] -> - # 2 element tuple - {end_location[:line] - 1, end_location[:column] - 1 + 1} - match?([_], args) -> - [literal] = args - delimiter = meta[:delimiter] - if delimiter in ["\"\"\"", "'''"] do - literal = if is_list(literal) do - to_string(literal) + {end_line, end_column} = + cond do + token = meta[:token] -> + {line, column + String.length(token)} + + end_location = meta[:closing] -> + # 2 element tuple + {end_location[:line] - 1, end_location[:column] - 1 + 1} + + match?([_], args) -> + [literal] = args + delimiter = meta[:delimiter] + + if delimiter in ["\"\"\"", "'''"] do + literal = + if is_list(literal) do + to_string(literal) + else + literal + end + + lines = SourceFile.lines(literal) + {line + length(lines), meta[:indentation] + get_delimiter_length(delimiter)} else - literal + get_literal_end(literal, {line, column}, delimiter) end - lines = SourceFile.lines(literal) - {line + length(lines), meta[:indentation] + get_delimiter_length(delimiter)} - else - get_literal_end(literal, {line, column}, delimiter) - end - - true -> - {line, column} - end + + true -> + {line, column} + end range(line, column, end_line, end_column) end end - # def node_range({left, right}) do - # case {node_range(left), node_range(right)} do - # {range(start_line, start_column, _, _), range(_, _, end_line, end_column)} -> - # range(start_line, start_column, end_line, end_column) - # _ -> nil - # end - # end - # def node_range([node]) do - # node_range(node) - # end - - # def node_range() - # interpolated charlist AST is too complicated to handle via the generic algorithm - def node_range({{:".", _, [List, :to_charlist]}, meta, _args} = ast) do + def node_range({{:., _, [List, :to_charlist]}, meta, _args} = ast) do line = Keyword.get(meta, :line) - 1 column = Keyword.get(meta, :column) - 1 {end_line, end_column} = get_eoe_by_formatting(ast, {line, column}) # formatter changes charlist '' to ~c"" sigil so we need to correct columns # if charlist is single line - correction = if end_line == line do - 2 - else - 0 - end + correction = + if end_line == line do + 2 + else + 0 + end + range(line, column, end_line, end_column - correction) end + # interpolated atom AST is too complicated to handle via the generic algorithm + def node_range({{:., _, [:erlang, :binary_to_atom]}, meta, _args} = ast) do + line = Keyword.get(meta, :line) - 1 + column = Keyword.get(meta, :column) - 1 + {end_line, end_column} = get_eoe_by_formatting(ast, {line, column}) + range(line, column, end_line, end_column) + end + def node_range({form, meta, args} = ast) do line = Keyword.get(meta, :line) column = Keyword.get(meta, :column) @@ -105,158 +116,177 @@ defmodule ElixirLS.LanguageServer.AstUtils do line = line - 1 column = column - 1 - dbg({line, column}) - - {start_line, start_column} = cond do - form == :"%{}" -> - # TODO elixir parser bug? - {line, column - 1} - form == :-> and match?([[_|_], _], args) -> - [[left | _], _right] = args - operator_length = form |> to_string() |> String.length() - case node_range(left) do - range(line, column, _, _) -> - {line, column} - nil -> - # TODO is it needed - {line, column} - end - form == :"&" and match?([int] when is_integer(int), args) -> - [int] = args - {line, column} - form in @binary_operators and match?([_, _], args) -> - [left, _right] = args - operator_length = form |> to_string() |> String.length() - - case node_range(left) do - range(line, column, _, _) -> - {line, column} - nil -> - # TODO is it needed - {line, column} - end - match?({:., _, [_ | _]}, form) -> - {:., _, [module_or_var | _]} = form - case node_range(module_or_var) do - range(line, column, _, _) -> - {line, column} - nil -> - # TODO is it needed - {line, column} - end - |> dbg - true -> {line, column} - end + {start_line, start_column} = + cond do + form == :%{} -> + # TODO elixir parser bug? + {line, column - 1} + + form == :-> and match?([[_ | _], _], args) -> + [[left | _], _right] = args + + case node_range(left) do + range(line, column, _, _) -> + {line, column} + + nil -> + # TODO is it needed + {line, column} + end + + form == :& and match?([int] when is_integer(int), args) -> + {line, column} - {end_line, end_column} = cond do - end_location = meta[:end] -> - {end_location[:line] - 1, end_location[:column] - 1 + 3} + form in @binary_operators and match?([_, _], args) -> + [left, _right] = args - end_location = meta[:end_of_expression] -> - {end_location[:line] - 1, end_location[:column] - 1} + case node_range(left) do + range(line, column, _, _) -> + {line, column} - end_location = meta[:closing] -> - closing_length = - case form do - :<<>> -> 2 - :fn -> 3 - _ -> 1 + nil -> + # TODO is it needed + {line, column} end - {end_location[:line] - 1, end_location[:column] - 1 + closing_length} + match?({:., _, [_ | _]}, form) -> + {:., _, [module_or_var | _]} = form - form == :__aliases__ -> - last = meta[:last] + case node_range(module_or_var) do + range(line, column, _, _) -> + {line, column} - last_length = - case List.last(args) do - atom when is_atom(atom) -> atom |> to_string() |> String.length() - _ -> 0 + nil -> + # TODO is it needed + {line, column} end - {last[:line] - 1, last[:column] - 1 + last_length} - - form == :"%" and match?([_, _], args) -> - [_alias, map] = args - case node_range(map) do - range(_, _, end_line, end_column) -> - {end_line, end_column} - nil -> - # TODO is it needed - {line, column} - end - - form == :"<<>>" or is_atom(form) and String.starts_with?(to_string(form), "sigil_") -> - # interpolated string AST is too complicated - # try to format it instead - get_eoe_by_formatting(ast, {line, column}) - - form == :"&" and match?([int] when is_integer(int), args) -> - [int] = args - {line, column + String.length(to_string(int))} - form in @binary_operators and match?([_, _], args) -> - [left, right] = args - operator_length = form |> to_string() |> String.length() - case node_range(right) do - range(_, _, end_line, end_column) -> - {end_line, end_column} - nil -> - # TODO is it needed - {line, column + operator_length} - end - - form in @unary_operators and match?([_], args) -> - [right] = args - operator_length = form |> to_string() |> String.length() - case node_range(right) do - range(_, _, end_line, end_column) -> - {end_line, end_column} - nil -> - # TODO is it needed - {line, column + operator_length} - end - - match?({:., _, [_, _ ]}, form) -> - case args do - [] -> - {:., _, [_, fun]} = form - # TODO handle quotes - {line, column + String.length(to_string(fun))} - _ -> - case node_range(List.last(args)) do - range(_, _, end_line, end_column) -> - {end_line, end_column} - nil -> - # TODO is it needed - nil - # {line, column + operator_length} + true -> + {line, column} + end + + {end_line, end_column} = + cond do + end_location = meta[:end] -> + {end_location[:line] - 1, end_location[:column] - 1 + 3} + + end_location = meta[:end_of_expression] -> + {end_location[:line] - 1, end_location[:column] - 1} + + end_location = meta[:closing] -> + closing_length = + case form do + :<<>> -> 2 + :fn -> 3 + _ -> 1 end - end - |> dbg - - is_atom(form) -> - variable_length = form |> to_string() |> String.length() - case args do - nil -> - {line, column + variable_length} - [] -> - {line, column + variable_length} - _ -> - # local call no parens - last_arg = List.last(args) - case node_range(last_arg |> dbg) |> dbg do - range(_, _, end_line, end_column) -> - {end_line, end_column} - nil -> - # TODO is it needed - {line, column + variable_length} + + {end_location[:line] - 1, end_location[:column] - 1 + closing_length} + + form == :__aliases__ -> + last = meta[:last] + + last_length = + case List.last(args) do + atom when is_atom(atom) -> atom |> to_string() |> String.length() + _ -> 0 end - end - - true -> - raise "unhandled block" - end + {last[:line] - 1, last[:column] - 1 + last_length} + + form == :% and match?([_, _], args) -> + [_alias, map] = args + + case node_range(map) do + range(_, _, end_line, end_column) -> + {end_line, end_column} + + nil -> + # TODO is it needed + {line, column} + end + + form == :<<>> or (is_atom(form) and String.starts_with?(to_string(form), "sigil_")) -> + # interpolated string AST is too complicated + # try to format it instead + get_eoe_by_formatting(ast, {line, column}) + + form == :& and match?([int] when is_integer(int), args) -> + [int] = args + {line, column + String.length(to_string(int))} + + form in @binary_operators and match?([_, _], args) -> + [_left, right] = args + operator_length = form |> to_string() |> String.length() + + case node_range(right) do + range(_, _, end_line, end_column) -> + {end_line, end_column} + + nil -> + # TODO is it needed + {line, column + operator_length} + end + + form in @unary_operators and match?([_], args) -> + [right] = args + operator_length = form |> to_string() |> String.length() + + case node_range(right) do + range(_, _, end_line, end_column) -> + {end_line, end_column} + + nil -> + # TODO is it needed + {line, column + operator_length} + end + + match?({:., _, [_, _]}, form) -> + case args do + [] -> + {:., _, [_, fun]} = form + # TODO handle quotes + {line, column + String.length(to_string(fun))} + + _ -> + case node_range(List.last(args)) do + range(_, _, end_line, end_column) -> + {end_line, end_column} + + nil -> + # TODO is it needed + nil + # {line, column + operator_length} + end + end + + is_atom(form) -> + variable_length = form |> to_string() |> String.length() + + case args do + nil -> + {line, column + variable_length} + + [] -> + {line, column + variable_length} + + _ -> + # local call no parens + last_arg = List.last(args) + + case node_range(last_arg) do + range(_, _, end_line, end_column) -> + {end_line, end_column} + + nil -> + # TODO is it needed + {line, column + variable_length} + end + end + + true -> + raise "unhandled block" + end range(start_line, start_column, end_line, end_column) end @@ -265,41 +295,50 @@ defmodule ElixirLS.LanguageServer.AstUtils do def get_literal_end(true, {line, column}, _), do: {line, column + 4} def get_literal_end(false, {line, column}, _), do: {line, column + 5} def get_literal_end(nil, {line, column}, _), do: {line, column + 3} + def get_literal_end(atom, {line, column}, delimiter) when is_atom(atom) do delimiter_length = get_delimiter_length(delimiter) - lines = atom |> to_string() |> SourceFile.lines + lines = atom |> to_string() |> SourceFile.lines() + case lines do [only_line] -> # add : {line, column + String.length(only_line) + 1 + 2 * delimiter_length} + _ -> - last_line_length = lines |> List.last |> String.length + last_line_length = lines |> List.last() |> String.length() {line + length(lines) - 1, last_line_length + 1 * delimiter_length} end end + def get_literal_end(list, {line, column}, delimiter) when is_list(list) do delimiter_length = get_delimiter_length(delimiter) - lines = list |> to_string() |> SourceFile.lines + lines = list |> to_string() |> SourceFile.lines() + case lines do [only_line] -> # add 2 x ' {line, column + String.length(only_line) + 2 * delimiter_length} + _ -> # add 1 x ' - last_line_length = lines |> List.last |> String.length + last_line_length = lines |> List.last() |> String.length() {line + length(lines) - 1, last_line_length + 1 * delimiter_length} end end + def get_literal_end(binary, {line, column}, delimiter) when is_binary(binary) do delimiter_length = get_delimiter_length(delimiter) - lines = binary |> SourceFile.lines + lines = binary |> SourceFile.lines() + case lines do [only_line] -> # add 2 x " {line, column + String.length(only_line) + 2 * delimiter_length} + _ -> # add 1 x " - last_line_length = lines |> List.last |> String.length + last_line_length = lines |> List.last() |> String.length() {line + length(lines) - 1, last_line_length + 1 * delimiter_length} end end @@ -311,10 +350,21 @@ defmodule ElixirLS.LanguageServer.AstUtils do def get_delimiter_length("'''"), do: 3 defp get_eoe_by_formatting(ast, {line, column}) do - code = ast |> Code.quoted_to_algebra |> Inspect.Algebra.format(:infinity) |> IO.iodata_to_binary() |> dbg + # TODO pass line_length to format + # TODO locals_without_parens to quoted_to_algebra + # TODO pass comments to quoted_to_algebra + code = + ast + |> Code.quoted_to_algebra(escape: false) + |> Inspect.Algebra.format(:infinity) + |> IO.iodata_to_binary() + lines = code |> SourceFile.lines() + case lines do - [_] -> {line, column + String.length(code)} + [_] -> + {line, column + String.length(code)} + _ -> last_line = List.last(lines) {line + length(lines) - 1, String.length(last_line)} diff --git a/apps/language_server/lib/language_server/providers/selection_ranges.ex b/apps/language_server/lib/language_server/providers/selection_ranges.ex index 3b6ea701c..75ddbd017 100644 --- a/apps/language_server/lib/language_server/providers/selection_ranges.ex +++ b/apps/language_server/lib/language_server/providers/selection_ranges.ex @@ -16,22 +16,18 @@ defmodule ElixirLS.LanguageServer.Providers.SelectionRanges do defp token_length(token) when token in [:"<<", :">>", :do, :fn], do: 2 defp token_length(_), do: 0 - # TODO => + # TODO => | @stop_tokens [:",", :";", :eol] - @binary_operators ~w[. ** * / + - ++ -- +++ --- .. <> in |> <<< >>> <<~ ~>> <~ ~> <~> < > <= >= == != === !== =~ && &&& and || ||| or = => :: when <- -> \\]a - @unary_operators ~w[@ + - ! ^ not &]a - @unary_and_binary_operators ~w[+ -]a - def selection_ranges(text, positions) do lines = SourceFile.lines(text) full_file_range = full_range(lines) - tokens = FoldingRange.Token.format_string(text) |> dbg(limit: :infinity) + tokens = FoldingRange.Token.format_string(text) - token_pairs = FoldingRange.TokenPair.pair_tokens(tokens) |> dbg + token_pairs = FoldingRange.TokenPair.pair_tokens(tokens) - stop_tokens = get_stop_tokens_in_token_pairs(tokens, token_pairs) |> dbg + stop_tokens = get_stop_tokens_in_token_pairs(tokens, token_pairs) special_token_groups = for group <- FoldingRange.SpecialToken.group_tokens(tokens) do @@ -52,7 +48,7 @@ defmodule ElixirLS.LanguageServer.Providers.SelectionRanges do literal_encoder: fn literal, meta -> {:ok, {:__block__, meta, [literal]}} end - ) |> dbg + ) cell_pairs = formatted_lines @@ -66,8 +62,9 @@ defmodule ElixirLS.LanguageServer.Providers.SelectionRanges do cell_pair_ranges = cell_pair_ranges(lines, cell_pairs, line, character) - token_pair_ranges = token_pair_ranges(lines, token_pairs, stop_tokens, line, character) - |> deduplicate + token_pair_ranges = + token_pair_ranges(lines, token_pairs, stop_tokens, line, character) + |> deduplicate special_token_group_ranges = special_token_group_ranges(special_token_groups, line, character) @@ -79,12 +76,12 @@ defmodule ElixirLS.LanguageServer.Providers.SelectionRanges do surround_context_ranges = surround_context_ranges(text, line, character) merged_ranges = - [full_file_range | token_pair_ranges] |> dbg - |> merge_ranges_lists([full_file_range | cell_pair_ranges] |> dbg) - |> merge_ranges_lists([full_file_range | special_token_group_ranges] |> dbg) - |> merge_ranges_lists([full_file_range | comment_block_ranges] |> dbg) - |> merge_ranges_lists([full_file_range | surround_context_ranges] |> dbg) - |> merge_ranges_lists([full_file_range | ast_node_ranges] |> dbg) + [full_file_range | token_pair_ranges] + |> merge_ranges_lists([full_file_range | cell_pair_ranges]) + |> merge_ranges_lists([full_file_range | special_token_group_ranges]) + |> merge_ranges_lists([full_file_range | comment_block_ranges]) + |> merge_ranges_lists([full_file_range | surround_context_ranges]) + |> merge_ranges_lists([full_file_range | ast_node_ranges]) if not increasingly_narrowing?(merged_ranges) do raise "merged_ranges are not increasingly narrowing" @@ -143,7 +140,7 @@ defmodule ElixirLS.LanguageServer.Providers.SelectionRanges do |> Enum.reduce([], fn {{start_token, {start_line, start_character, _}, _}, {end_token, {end_line, end_character, _}, _}} = pair, acc -> - stop_tokens_in_pair = Map.get(stop_tokens, pair, []) |> dbg + stop_tokens_in_pair = Map.get(stop_tokens, pair, []) start_token_length = token_length(start_token) end_token_length = token_length(end_token) @@ -160,8 +157,9 @@ defmodule ElixirLS.LanguageServer.Providers.SelectionRanges do else line_length = lines |> Enum.at(end_line - 1) |> String.length() inner_range = range(start_line + 1, 0, end_line - 1, line_length) + find_stop_token_range(stop_tokens_in_pair, pair, inner_range, line, character) ++ - [inner_range, outer_range | acc] + [inner_range, outer_range | acc] end _ -> @@ -172,17 +170,19 @@ defmodule ElixirLS.LanguageServer.Providers.SelectionRanges do # ^ ^ [outer_range | acc] else - inner_range = range( - start_line, - start_character + start_token_length, - end_line, - end_character - ) + inner_range = + range( + start_line, + start_character + start_token_length, + end_line, + end_character + ) + find_stop_token_range(stop_tokens_in_pair, pair, inner_range, line, character) ++ - [ - inner_range, - outer_range | acc - ] + [ + inner_range, + outer_range | acc + ] end end end) @@ -190,42 +190,66 @@ defmodule ElixirLS.LanguageServer.Providers.SelectionRanges do end defp find_stop_token_range([], _, _, _, _), do: [] + defp find_stop_token_range(tokens, {begin_token, end_token}, inner_range, line, character) do - {_, found} = Enum.reduce_while(tokens ++ [{end_token, nil, nil}], {{begin_token, nil, nil}, []}, fn - {token, before_stop, _} = token_tuple, {{previous_token, _, after_previous}, _} -> - {_, {start_line, start_character, _}, _} = previous_token - {_, {end_line, end_character, _}, _} = token - if (start_line < line or start_line == line and start_character <= character) and (end_line > line or end_line == line and end_character >= character) do - dbg({previous_token, after_previous, before_stop, token}) - {end_line, end_character} = case before_stop do - {kind, _, _} when kind in [:bin_string, :list_string] -> - {end_line, end_character} - {kind, {before_start_line, before_start_character, list}, _} when is_list(list) -> - length_modifier = if kind == :atom do - 1 - else - 0 + {_, found} = + Enum.reduce_while(tokens ++ [{end_token, nil, nil}], {{begin_token, nil, nil}, []}, fn + {token, before_stop, _} = token_tuple, {{previous_token, _, after_previous}, _} -> + {_, {start_line, start_character, _}, _} = previous_token + {_, {end_line, end_character, _}, _} = token + + if (start_line < line or (start_line == line and start_character <= character)) and + (end_line > line or (end_line == line and end_character >= character)) do + # dbg({previous_token, after_previous, before_stop, token}) + {end_line, end_character} = + case before_stop do + {kind, _, _} when kind in [:bin_string, :list_string] -> + {end_line, end_character} + + {kind, {before_start_line, before_start_character, list}, _} when is_list(list) -> + length_modifier = + if kind == :atom do + 1 + else + 0 + end + + {before_start_line, before_start_character + length(list) + length_modifier} + + {_, {before_start_line, before_start_character, _}, list} when is_list(list) -> + {before_start_line, before_start_character + length(list)} + + {:atom_quoted, {before_start_line, before_start_character, _}, atom} -> + {before_start_line, before_start_character + String.length(to_string(atom)) + 3} + + _ -> + {end_line, end_character} end - {before_start_line, before_start_character + length(list) + length_modifier} - {_, {before_start_line, before_start_character, _}, list} when is_list(list) -> - {before_start_line, before_start_character + length(list)} - {:atom_quoted, {before_start_line, before_start_character, _}, atom} -> - {before_start_line, before_start_character + String.length(to_string(atom)) + 3} - _ -> {end_line, end_character} - end - {start_line, start_character} = case after_previous do - {_, {after_end_line, after_end_character, _}, _} -> - {after_end_line, after_end_character} - nil -> - {start_line, start_character} + + {start_line, start_character} = + case after_previous do + {_, {after_end_line, after_end_character, _}, _} -> + {after_end_line, after_end_character} + + nil -> + {start_line, start_character} + end + + # TODO + {:halt, + {token_tuple, + [ + intersection( + range(start_line, start_character, end_line, end_character), + inner_range + ) + ]}} + else + {:cont, {token_tuple, []}} end - # TODO - {:halt, {token_tuple, [intersection(range(start_line, start_character, end_line, end_character), inner_range)]}} - else - {:cont, {token_tuple, []}} - end - end) - found |> dbg + end) + + found end def cell_pair_ranges(lines, cell_pairs, line, character) do @@ -321,165 +345,41 @@ defmodule ElixirLS.LanguageServer.Providers.SelectionRanges do ast, {[], []}, fn - {form, meta, args} = ast, {acc, parent_ast} -> + {form, _meta, _args} = ast, {acc, parent_ast} -> parent_ast_from_stack = case parent_ast do [] -> [] [item | _] -> item end - # {start_line, start_character} = - # cond do - # node == :%{} and match?({:%, _, _}, parent_ast_from_stack) -> - # # get line and column from parent % node, current node meta points to { - # {_, parent_meta, _} = parent_ast_from_stack - - # {Keyword.get(parent_meta, :line, 1) - 1, - # Keyword.get(parent_meta, :column, 1) - 1} - - # node in @binary_operators and match?([_, _], args) -> - # [left | _] = args - # # dbg(binding(), limit: :infinity) - # find_start_of_expression(left, {Keyword.get(meta, :line, 1) - 1, Keyword.get(meta, :column, 1) - 1}) - - - - # meta_line = meta[:line] -> - # {meta_line - 1, Keyword.get(meta, :column, 1) - 1} - - # true -> - # find_start_of_expression(ast, {nil, nil}) - # end - - # if start_line < 0 or start_character < 0 do - # dbg(ast) - # raise "could not find start of expression" - # end - - # {end_line, end_character} = - # cond do - # node == :__aliases__ -> - # last = meta[:last] - - # last_length = - # case List.last(args) do - # atom when is_atom(atom) -> atom |> to_string() |> String.length() - # _ -> 0 - # end - - # {last[:line] - 1, last[:column] - 1 + last_length} - - # node in @binary_operators and match?([_, _], args) -> - # dbg(ast) - # dbg(parent_ast_from_stack) - # [_left, right] = args - # operator_length = node|> to_string() |> String.length() - # find_end_of_expression(right, parent_ast_from_stack, {start_line, start_character + operator_length}) - - # node in @unary_operators and match?([_], args) -> - # [right] = args - # operator_length = node|> to_string() |> String.length() - # find_end_of_expression(right, parent_ast_from_stack, {start_line, start_character + operator_length}) - - # end_location = meta[:end_of_expression] -> - # {end_location[:line] - 1, end_location[:column] - 1} - - # end_location = meta[:end] -> - # {end_location[:line] - 1, end_location[:column] - 1 + 3} - - # end_location = meta[:closing] -> - # closing_length = - # case node do - # :<<>> -> 2 - # _ -> 1 - # end - - # {end_location[:line] - 1, end_location[:column] - 1 + closing_length} - - # token = meta[:token] -> - # {start_line, start_character + String.length(token)} - - # meta[:delimiter] && (is_list(node) or is_binary(node)) -> - # {start_line, start_character + String.length(to_string(node))} - - # # TODO a few other cases - - # # parent_end_line = - # # parent_meta_from_stack - # # |> dbg() - # # |> Keyword.get(:end, []) - # # |> Keyword.get(:line) -> - # # # last expression in block does not have end_of_expression - # # parent_do_line = parent_meta_from_stack[:do][:line] - - # # if parent_end_line > parent_do_line do - # # # take end location from parent and assume end_of_expression is last char in previous line - # # end_of_expression = - # # Enum.at(lines, max(parent_end_line - 2, 0)) - # # |> String.length() - - # # SourceFile.elixir_position_to_lsp( - # # lines, - # # {parent_end_line - 1, end_of_expression + 1} - # # ) - # # else - # # # take end location from parent and assume end_of_expression is last char before final ; trimmed - # # line = Enum.at(lines, parent_end_line - 1) - # # parent_end_column = parent_meta_from_stack[:end][:column] - - # # end_of_expression = - # # line - # # |> String.slice(0..(parent_end_column - 2)) - # # |> String.trim_trailing() - # # |> String.replace_trailing(";", "") - # # |> String.length() - - # # SourceFile.elixir_position_to_lsp( - # # lines, - # # {parent_end_line, end_of_expression + 1} - # # ) - # # end - # true -> - # find_end_of_expression(ast, parent_ast_from_stack, {start_line, start_character}) - # end - - range = AstUtils.node_range(ast) |> dbg - # range = try do - # AstUtils.node_range(ast) - # rescue - # _ -> nil - # end - - case range do - range(start_line, start_character, end_line, end_character) = range -> - start_character = if form == :"%{}" and match?({:%, _, _}, parent_ast_from_stack) do - # undo column offset for structs inner map node - start_character + 1 - else - start_character - end + case AstUtils.node_range(ast) do + range(start_line, start_character, end_line, end_character) -> + start_character = + if form == :%{} and match?({:%, _, _}, parent_ast_from_stack) do + # undo column offset for structs inner map node + start_character + 1 + else + start_character + end + range = range(start_line, start_character, end_line, end_character) + if (start_line < line or (start_line == line and start_character <= character)) and - (end_line > line or (end_line == line and end_character >= character)) do - # range = range(start_line, start_character, end_line, end_character) - if not valid?(range) do - raise "invalid range" - end + (end_line > line or (end_line == line and end_character >= character)) do # dbg({ast, range, parent_ast_from_stack}) - {ast, - {[range | acc], - [ast | parent_ast]}} + {ast, {[range | acc], [ast | parent_ast]}} else - dbg({ast, range, {line, character}, "outside"}) + # dbg({ast, range, {line, character}, "outside"}) {ast, {acc, [ast | parent_ast]}} end + nil -> - dbg({ast, "nil"}) + # dbg({ast, "nil"}) {ast, {acc, [ast | parent_ast]}} - end + end other, {acc, parent_ast} -> - dbg({other, "other"}) + # dbg({other, "other"}) {other, {acc, parent_ast}} end, fn @@ -511,17 +411,24 @@ defmodule ElixirLS.LanguageServer.Providers.SelectionRanges do tokens_next = tl(tokens) ++ [nil] tokens_prev = [nil | Enum.slice(tokens, 0..-2//1)] tokens_prev_next = Enum.zip([tokens_prev, tokens, tokens_next]) - for {prev_token, {token, {line, character, _}, _} = token_tuple, next_token} <- tokens_prev_next, - token in @stop_tokens - do - pair = token_pairs - |> Enum.filter(fn {{_, {start_line, start_character, _}, _}, {_, {end_line, end_character, _}, _}} -> - (start_line < line or start_line == line and start_character <= character) and (end_line > line or end_line == line and end_character >= character) - end) - |> Enum.min_by(fn {{_, {start_line, start_character, _}, _}, {_, {end_line, end_character,_}, _}} -> - {end_line - start_line, end_character - start_character} - end, &<=/2, - fn -> nil end) + + for {prev_token, {token, {line, character, _}, _} = token_tuple, next_token} <- + tokens_prev_next, + token in @stop_tokens do + pair = + token_pairs + |> Enum.filter(fn {{_, {start_line, start_character, _}, _}, + {_, {end_line, end_character, _}, _}} -> + (start_line < line or (start_line == line and start_character <= character)) and + (end_line > line or (end_line == line and end_character >= character)) + end) + |> Enum.min_by( + fn {{_, {start_line, start_character, _}, _}, {_, {end_line, end_character, _}, _}} -> + {end_line - start_line, end_character - start_character} + end, + &<=/2, + fn -> nil end + ) {pair, {token_tuple, prev_token, next_token}} end @@ -531,127 +438,4 @@ defmodule ElixirLS.LanguageServer.Providers.SelectionRanges do end) |> Map.new() end - - def find_start_of_expression(ast, acc) do - {_, soe} = Macro.prewalk(ast, acc, fn - {kind, meta, _} = node, {line, column} -> - if soe_line = meta[:line] do - soe_line = soe_line - 1 - correction = if kind == :"%{}" do - # TODO is is a bug in parser? column is invalid - -1 - else - 0 - end - soe_column = meta[:column] - 1 + correction - if soe_line < line or (soe_line == line and soe_column <= column) do - {node, {soe_line, soe_column}} - else - {node, {line, column}} - end - else - {node, {line, column}} - end - node, acc -> - {node, acc} - end) - dbg({ast, soe}) - case soe do - {nil, nil} -> :ok - {l, c} when l < 0 or c < 0 -> raise "invalid start of expression" - _ -> :ok - end - soe - end - - defp find_end_of_expression(ast, parent_node, acc) do - {soe_line, soe_column} = find_start_of_expression(ast, {nil, nil}) - {soe_line, soe_column} = if {soe_line, soe_column} == {nil, nil} do - acc - else - {soe_line, soe_column} - end - - # try to find it in meta recursively - {_, eoe} = Macro.prewalk(ast, acc, fn - {node, meta, args} = ast, {line, column} -> - {eoe_line, eoe_column} = cond do - node == :__aliases__ -> - last = meta[:last] - - last_length = - case List.last(args) do - atom when is_atom(atom) -> atom |> to_string() |> String.length() - _ -> 0 - end - - {last[:line] - 1, last[:column] - 1 + last_length} - - end_location = meta[:end_of_expression] -> - {end_location[:line] - 1, end_location[:column] - 1} - - end_location = meta[:end] -> - {end_location[:line] - 1, end_location[:column] - 1 + 3} - - end_location = meta[:closing] -> - closing_length = - case node do - :<<>> -> 2 - _ -> 1 - end - - {end_location[:line] - 1, end_location[:column] - 1 + closing_length} - - token = meta[:token] -> - {soe_line, soe_column + String.length(token)} - - meta[:delimiter] && (is_list(node) or is_binary(node)) -> - {soe_line, soe_column + String.length(to_string(node))} - - true -> - {line, column} - end - - if eoe_line > line or (eoe_line == line and eoe_column >= column) do - {node, {eoe_line, eoe_column}} - else - {node, {line, column}} - end - node, acc -> - {node, acc} - end) - - eoe = if eoe == acc do - # no end_of_expression in last expression in do block - # get from parent meta - case parent_node do - {_, parent_meta, _} -> - end_meta = parent_meta[:end] - if end_meta do - {end_meta[:line] - 1, end_meta[:column] - 1} - else - acc - end - _ -> acc - end - else - eoe - end - - # try to format the expression and count chars - eoe = if eoe == acc do - code = ast |> dbg |> Code.quoted_to_algebra |> Inspect.Algebra.format(:infinity) |> IO.iodata_to_binary() - lines = code |> SourceFile.lines() - case lines do - [_] -> {soe_line, soe_column + String.length(code)} - _ -> - last_line = Enum.at(lines, -1) - {soe_line + length(lines) - 1, String.length(last_line)} - end - else - eoe - end - dbg({ast, eoe}) - eoe - end end diff --git a/apps/language_server/lib/language_server/range_utils.ex b/apps/language_server/lib/language_server/range_utils.ex index e060d0c85..d031abb36 100644 --- a/apps/language_server/lib/language_server/range_utils.ex +++ b/apps/language_server/lib/language_server/range_utils.ex @@ -160,10 +160,12 @@ defmodule ElixirLS.LanguageServer.RangeUtils do end defp do_deduplicate([], acc), do: acc + defp do_deduplicate([range | rest], [range | _] = acc) do do_deduplicate(rest, acc) end - defp do_deduplicate([range | rest], acc) do + + defp do_deduplicate([range | rest], acc) do do_deduplicate(rest, [range | acc]) end end diff --git a/apps/language_server/test/ast_utils_test.exs b/apps/language_server/test/ast_utils_test.exs index f45d08f0a..9f14bc931 100644 --- a/apps/language_server/test/ast_utils_test.exs +++ b/apps/language_server/test/ast_utils_test.exs @@ -5,9 +5,17 @@ defmodule ElixirLS.LanguageServer.AstUtilsTest do import ElixirLS.LanguageServer.AstUtils defp get_range(code) do - IO.puts(code) - {:ok, ast} = Code.string_to_quoted(code, columns: true, token_metadata: true, unescape: false, literal_encoder: &{:ok, {:__block__, &2, [&1]}}) - dbg(ast) + # IO.puts(code) + + {:ok, ast} = + Code.string_to_quoted(code, + columns: true, + token_metadata: true, + unescape: false, + literal_encoder: &{:ok, {:__block__, &2, [&1]}} + ) + + # dbg(ast) node_range(ast) end @@ -44,6 +52,14 @@ defmodule ElixirLS.LanguageServer.AstUtilsTest do assert get_range(":'abc'") == range(0, 0, 0, 6) end + test "quoted atom string interpolated" do + assert get_range(":\"ab\#{inspect(self())}c\"") == range(0, 0, 0, 24) + end + + test "quoted atom charlist interpolated" do + assert get_range(":'ab\#{inspect(self())}c'") == range(0, 0, 0, 24) + end + test "string" do assert get_range("\"abc\"") == range(0, 0, 0, 5) end @@ -84,6 +100,14 @@ defmodule ElixirLS.LanguageServer.AstUtilsTest do assert get_range("'abc \#{inspect(a)} sd'") == range(0, 0, 0, 22) end + test "string heredoc interpolated" do + assert get_range("\"\"\"\nab\#{inspect(a)}c\n\"\"\"") == range(0, 0, 2, 3) + end + + test "charlist heredoc interpolated" do + assert get_range("'''\nab\#{inspect(a)}c\n'''") == range(0, 0, 2, 3) + end + test "sigil" do assert get_range("~w(asd fgh)") == range(0, 0, 0, 11) end @@ -93,16 +117,17 @@ defmodule ElixirLS.LanguageServer.AstUtilsTest do end test "sigil with interpolation" do - text = "~s(asd " <> "\#" <> "{" <> "inspect(self())" <> "} fgh)" + text = "~s(asd \#{inspect(self())} fgh)" assert get_range(text) == range(0, 0, 0, 30) end - + test "sigil with heredoc string" do text = """ ~S\"\"\" some text \"\"\" """ + assert get_range(text) == range(0, 0, 2, 3) end @@ -112,6 +137,7 @@ defmodule ElixirLS.LanguageServer.AstUtilsTest do some text ''' """ + assert get_range(text) == range(0, 0, 2, 3) end @@ -300,15 +326,20 @@ defmodule ElixirLS.LanguageServer.AstUtilsTest do assert get_range(":some.fun(123)") == range(0, 0, 0, 14) end - test "remote call quoted" do + test "remote call quoted string" do assert get_range("Some.\"0fun\"(123)") == range(0, 0, 0, 16) end + test "remote call quoted charlist" do + assert get_range("Some.'0fun'(123)") == range(0, 0, 0, 16) + end + test "remote call pipe" do text = """ 123 |> Some.fun1() """ + assert get_range(text) == range(0, 0, 1, 14) end @@ -317,6 +348,7 @@ defmodule ElixirLS.LanguageServer.AstUtilsTest do 123 |> Some.fun1 """ + assert get_range(text) == range(0, 0, 1, 12) end @@ -325,6 +357,7 @@ defmodule ElixirLS.LanguageServer.AstUtilsTest do 123 |> local() """ + assert get_range(text) == range(0, 0, 1, 10) end @@ -333,6 +366,7 @@ defmodule ElixirLS.LanguageServer.AstUtilsTest do 123 |> local """ + assert get_range(text) == range(0, 0, 1, 8) end @@ -378,6 +412,7 @@ defmodule ElixirLS.LanguageServer.AstUtilsTest do test = """ fn -> 1 end """ + assert get_range(test) == range(0, 0, 0, 11) end @@ -385,6 +420,7 @@ defmodule ElixirLS.LanguageServer.AstUtilsTest do test = """ fn a, b -> 1 end """ + assert get_range(test) == range(0, 0, 0, 16) end @@ -395,6 +431,7 @@ defmodule ElixirLS.LanguageServer.AstUtilsTest do _ -> 2 end """ + assert get_range(test) == range(0, 0, 3, 3) end @@ -404,6 +441,7 @@ defmodule ElixirLS.LanguageServer.AstUtilsTest do x end """ + assert get_range(text) == range(0, 0, 2, 3) end @@ -412,7 +450,8 @@ defmodule ElixirLS.LanguageServer.AstUtilsTest do defp name(%Config{} = config), do: :"#{__MODULE__}_#{config.node_id}_#{config.channel_unique_id}" """ - assert get_range(test) == range(0, 0, 1, 39) + + assert get_range(test) == range(0, 0, 1, 68) end end end diff --git a/apps/language_server/test/providers/selection_ranges_test.exs b/apps/language_server/test/providers/selection_ranges_test.exs index d66514a47..af70f46b7 100644 --- a/apps/language_server/test/providers/selection_ranges_test.exs +++ b/apps/language_server/test/providers/selection_ranges_test.exs @@ -21,8 +21,10 @@ defmodule ElixirLS.LanguageServer.Providers.SelectionRangesTest do flatten(parent, [range | acc]) end - defp assert_range(ranges, expected) do - assert Enum.any?(ranges, & &1 == expected) + defmacrop assert_range(ranges, expected) do + quote do + assert Enum.any?(unquote(ranges), &(&1 == unquote(expected))) + end end describe "token pair ranges" do @@ -34,15 +36,15 @@ defmodule ElixirLS.LanguageServer.Providers.SelectionRangesTest do ranges = get_ranges(text, 0, 3) # full range - assert_range ranges, range(0, 0, 1, 0) + assert_range(ranges, range(0, 0, 1, 0)) # [] outside - assert_range ranges, range(0, 0, 0, 11) + assert_range(ranges, range(0, 0, 0, 11)) # [] inside - assert_range ranges, range(0, 1, 0, 10) + assert_range(ranges, range(0, 1, 0, 10)) # {} outside - assert_range ranges, range(0, 1, 0, 7) + assert_range(ranges, range(0, 1, 0, 7)) # {} inside - assert_range ranges, range(0, 2, 0, 6) + assert_range(ranges, range(0, 2, 0, 6)) end test "brackets cursor inside left" do @@ -53,11 +55,11 @@ defmodule ElixirLS.LanguageServer.Providers.SelectionRangesTest do ranges = get_ranges(text, 0, 1) # full range - assert_range ranges, range(0, 0, 1, 0) + assert_range(ranges, range(0, 0, 1, 0)) # {} outside - assert_range ranges, range(0, 0, 0, 6) + assert_range(ranges, range(0, 0, 0, 6)) # {} inside - assert_range ranges, range(0, 1, 0, 5) + assert_range(ranges, range(0, 1, 0, 5)) end test "brackets cursor inside right" do @@ -68,11 +70,11 @@ defmodule ElixirLS.LanguageServer.Providers.SelectionRangesTest do ranges = get_ranges(text, 0, 5) # full range - assert_range ranges, range(0, 0, 1, 0) + assert_range(ranges, range(0, 0, 1, 0)) # {} outside - assert_range ranges, range(0, 0, 0, 6) + assert_range(ranges, range(0, 0, 0, 6)) # {} inside - assert_range ranges, range(0, 1, 0, 5) + assert_range(ranges, range(0, 1, 0, 5)) end test "brackets cursor outside left" do @@ -83,9 +85,9 @@ defmodule ElixirLS.LanguageServer.Providers.SelectionRangesTest do ranges = get_ranges(text, 0, 0) # full range - assert_range ranges, range(0, 0, 1, 0) + assert_range(ranges, range(0, 0, 1, 0)) # {} outside - assert_range ranges, range(0, 0, 0, 6) + assert_range(ranges, range(0, 0, 0, 6)) end test "brackets cursor outside right" do @@ -96,9 +98,9 @@ defmodule ElixirLS.LanguageServer.Providers.SelectionRangesTest do ranges = get_ranges(text, 0, 0) # full range - assert_range ranges, range(0, 0, 1, 0) + assert_range(ranges, range(0, 0, 1, 0)) # {} outside - assert_range ranges, range(0, 0, 0, 6) + assert_range(ranges, range(0, 0, 0, 6)) end end @@ -110,9 +112,9 @@ defmodule ElixirLS.LanguageServer.Providers.SelectionRangesTest do ranges = get_ranges(text, 0, 1) # full range - assert_range ranges, range(0, 0, 1, 0) + assert_range(ranges, range(0, 0, 1, 0)) # full alias - assert_range ranges, range(0, 0, 0, 15) + assert_range(ranges, range(0, 0, 0, 15)) end test "remote call" do @@ -123,11 +125,11 @@ defmodule ElixirLS.LanguageServer.Providers.SelectionRangesTest do ranges = get_ranges(text, 0, 17) # full range - assert_range ranges, range(0, 0, 1, 0) + assert_range(ranges, range(0, 0, 1, 0)) # full remote call - assert_range ranges, range(0, 0, 0, 26) + assert_range(ranges, range(0, 0, 0, 26)) # full remote call - assert_range ranges, range(0, 0, 0, 24) + assert_range(ranges, range(0, 0, 0, 24)) end describe "comments" do @@ -139,11 +141,11 @@ defmodule ElixirLS.LanguageServer.Providers.SelectionRangesTest do ranges = get_ranges(text, 0, 5) # full range - assert_range ranges, range(0, 0, 1, 0) + assert_range(ranges, range(0, 0, 1, 0)) # full line - assert_range ranges, range(0, 0, 0, 16) + assert_range(ranges, range(0, 0, 0, 16)) # from # - assert_range ranges, range(0, 2, 0, 16) + assert_range(ranges, range(0, 2, 0, 16)) end test "comment block on first line" do @@ -156,13 +158,13 @@ defmodule ElixirLS.LanguageServer.Providers.SelectionRangesTest do ranges = get_ranges(text, 0, 5) # full range - assert_range ranges, range(0, 0, 3, 0) + assert_range(ranges, range(0, 0, 3, 0)) # full lines - assert_range ranges, range(0, 0, 2, 13) + assert_range(ranges, range(0, 0, 2, 13)) # from # - assert_range ranges, range(0, 2, 2, 13) + assert_range(ranges, range(0, 2, 2, 13)) # from # first line - assert_range ranges, range(0, 2, 0, 16) + assert_range(ranges, range(0, 2, 0, 16)) end test "comment block on middle line" do @@ -175,15 +177,15 @@ defmodule ElixirLS.LanguageServer.Providers.SelectionRangesTest do ranges = get_ranges(text, 1, 5) # full range - assert_range ranges, range(0, 0, 3, 0) + assert_range(ranges, range(0, 0, 3, 0)) # full lines - assert_range ranges, range(0, 0, 2, 13) + assert_range(ranges, range(0, 0, 2, 13)) # from # - assert_range ranges, range(0, 2, 2, 13) + assert_range(ranges, range(0, 2, 2, 13)) # full # middle line - assert_range ranges, range(1, 0, 1, 18) + assert_range(ranges, range(1, 0, 1, 18)) # from # middle line - assert_range ranges, range(1, 2, 1, 18) + assert_range(ranges, range(1, 2, 1, 18)) end test "comment block on last line" do @@ -196,15 +198,15 @@ defmodule ElixirLS.LanguageServer.Providers.SelectionRangesTest do ranges = get_ranges(text, 2, 5) # full range - assert_range ranges, range(0, 0, 3, 0) + assert_range(ranges, range(0, 0, 3, 0)) # full lines - assert_range ranges, range(0, 0, 2, 13) + assert_range(ranges, range(0, 0, 2, 13)) # from # - assert_range ranges, range(0, 2, 2, 13) + assert_range(ranges, range(0, 2, 2, 13)) # full # last line - assert_range ranges, range(2, 0, 2, 13) + assert_range(ranges, range(2, 0, 2, 13)) # from # last line - assert_range ranges, range(2, 2, 2, 13) + assert_range(ranges, range(2, 2, 2, 13)) end end @@ -219,11 +221,11 @@ defmodule ElixirLS.LanguageServer.Providers.SelectionRangesTest do ranges = get_ranges(text, 1, 1) # full range - assert_range ranges, range(0, 0, 4, 0) + assert_range(ranges, range(0, 0, 4, 0)) # outside do-end - assert_range ranges, range(0, 0, 3, 3) + assert_range(ranges, range(0, 0, 3, 3)) # inside do-end - assert_range ranges, range(1, 0, 2, 4) + assert_range(ranges, range(1, 0, 2, 4)) end test "left from do" do @@ -236,11 +238,11 @@ defmodule ElixirLS.LanguageServer.Providers.SelectionRangesTest do ranges = get_ranges(text, 0, 0) # full range - assert_range ranges, range(0, 0, 4, 0) + assert_range(ranges, range(0, 0, 4, 0)) # outside do-end - assert_range ranges, range(0, 0, 3, 3) + assert_range(ranges, range(0, 0, 3, 3)) # do - assert_range ranges, range(0, 0, 0, 2) + assert_range(ranges, range(0, 0, 0, 2)) end test "right from do" do @@ -253,9 +255,9 @@ defmodule ElixirLS.LanguageServer.Providers.SelectionRangesTest do ranges = get_ranges(text, 0, 2) # full range - assert_range ranges, range(0, 0, 4, 0) + assert_range(ranges, range(0, 0, 4, 0)) # outside do-end - assert_range ranges, range(0, 0, 3, 3) + assert_range(ranges, range(0, 0, 3, 3)) end test "left from end" do @@ -268,11 +270,11 @@ defmodule ElixirLS.LanguageServer.Providers.SelectionRangesTest do ranges = get_ranges(text, 3, 0) # full range - assert_range ranges, range(0, 0, 4, 0) + assert_range(ranges, range(0, 0, 4, 0)) # outside do-end - assert_range ranges, range(0, 0, 3, 3) + assert_range(ranges, range(0, 0, 3, 3)) # end - assert_range ranges, range(3, 0, 3, 3) + assert_range(ranges, range(3, 0, 3, 3)) end test "right from end" do @@ -285,9 +287,9 @@ defmodule ElixirLS.LanguageServer.Providers.SelectionRangesTest do ranges = get_ranges(text, 3, 3) # full range - assert_range ranges, range(0, 0, 4, 0) + assert_range(ranges, range(0, 0, 4, 0)) # outside do-end - assert_range ranges, range(0, 0, 3, 3) + assert_range(ranges, range(0, 0, 3, 3)) end end @@ -302,11 +304,11 @@ defmodule ElixirLS.LanguageServer.Providers.SelectionRangesTest do ranges = get_ranges(text, 2, 4) # full range - assert_range ranges, range(0, 0, 5, 0) + assert_range(ranges, range(0, 0, 5, 0)) # defmodule - assert_range ranges, range(0, 0, 4, 3) + assert_range(ranges, range(0, 0, 4, 3)) # def - assert_range ranges, range(1, 2, 3, 5) + assert_range(ranges, range(1, 2, 3, 5)) end describe "doc" do @@ -319,9 +321,9 @@ defmodule ElixirLS.LanguageServer.Providers.SelectionRangesTest do ranges = get_ranges(text, 1, 0) # full range - assert_range ranges, range(0, 0, 3, 0) + assert_range(ranges, range(0, 0, 3, 0)) # full @doc - assert_range ranges, range(0, 0, 2, 3) + assert_range(ranges, range(0, 0, 2, 3)) end test "heredoc" do @@ -333,9 +335,9 @@ defmodule ElixirLS.LanguageServer.Providers.SelectionRangesTest do ranges = get_ranges(text, 1, 0) # full range - assert_range ranges, range(0, 0, 3, 0) + assert_range(ranges, range(0, 0, 3, 0)) # full @doc - assert_range ranges, range(0, 0, 2, 3) + assert_range(ranges, range(0, 0, 2, 3)) end test "charlist heredoc" do @@ -347,9 +349,9 @@ defmodule ElixirLS.LanguageServer.Providers.SelectionRangesTest do ranges = get_ranges(text, 1, 0) # full range - assert_range ranges, range(0, 0, 3, 0) + assert_range(ranges, range(0, 0, 3, 0)) # full @doc - assert_range ranges, range(0, 0, 2, 3) + assert_range(ranges, range(0, 0, 2, 3)) end end @@ -363,9 +365,9 @@ defmodule ElixirLS.LanguageServer.Providers.SelectionRangesTest do ranges = get_ranges(text, 1, 0) # full range - assert_range ranges, range(0, 0, 3, 0) + assert_range(ranges, range(0, 0, 3, 0)) # full literal - assert_range ranges, range(0, 2, 2, 3) + assert_range(ranges, range(0, 2, 2, 3)) end test "number" do @@ -375,11 +377,11 @@ defmodule ElixirLS.LanguageServer.Providers.SelectionRangesTest do ranges = get_ranges(text, 0, 0) # full range - assert_range ranges, range(0, 0, 1, 0) + assert_range(ranges, range(0, 0, 1, 0)) # full expression - assert_range ranges, range(0, 0, 0, 9) + assert_range(ranges, range(0, 0, 0, 9)) # full literal - assert_range ranges, range(0, 0, 0, 4) + assert_range(ranges, range(0, 0, 0, 4)) end test "atom" do @@ -389,9 +391,9 @@ defmodule ElixirLS.LanguageServer.Providers.SelectionRangesTest do ranges = get_ranges(text, 0, 1) # full range - assert_range ranges, range(0, 0, 1, 0) + assert_range(ranges, range(0, 0, 1, 0)) # full literal - assert_range ranges, range(0, 0, 0, 8) + assert_range(ranges, range(0, 0, 0, 8)) end test "interpolated string" do @@ -401,18 +403,18 @@ defmodule ElixirLS.LanguageServer.Providers.SelectionRangesTest do ranges = get_ranges(text, 0, 17) # full range - assert_range ranges, range(0, 0, 1, 0) + assert_range(ranges, range(0, 0, 1, 0)) # full literal - assert_range ranges, range(0, 0, 0, 28) + assert_range(ranges, range(0, 0, 0, 28)) # full interpolation - assert_range ranges, range(0, 5, 0, 23) + assert_range(ranges, range(0, 5, 0, 23)) # inside #{} - assert_range ranges, range(0, 7, 0, 22) + assert_range(ranges, range(0, 7, 0, 22)) # inside () - assert_range ranges, range(0, 15, 0, 21) + assert_range(ranges, range(0, 15, 0, 21)) # literal # NOTE AST only matching - no tokens inside interpolation - assert_range ranges, range(0, 16, 0, 17) + assert_range(ranges, range(0, 16, 0, 17)) end end @@ -424,7 +426,7 @@ defmodule ElixirLS.LanguageServer.Providers.SelectionRangesTest do ranges = get_ranges(text, 0, 1) # full range - assert_range ranges, range(0, 0, 1, 0) + assert_range(ranges, range(0, 0, 1, 0)) # utf16 range assert range(0, 0, 0, end_character) = Enum.at(ranges, 1) @@ -443,21 +445,21 @@ defmodule ElixirLS.LanguageServer.Providers.SelectionRangesTest do ranges = get_ranges(text, 1, 2) # full range - assert_range ranges, range(0, 0, 4, 0) + assert_range(ranges, range(0, 0, 4, 0)) # full struct - assert_range ranges, range(0, 0, 3, 1) + assert_range(ranges, range(0, 0, 3, 1)) # full {} outside - assert_range ranges, range(0, 10, 3, 1) + assert_range(ranges, range(0, 10, 3, 1)) # full {} inside - assert_range ranges, range(0, 11, 3, 0) + assert_range(ranges, range(0, 11, 3, 0)) # full lines: - assert_range ranges, range(1, 0, 2, 14) + assert_range(ranges, range(1, 0, 2, 14)) # full lines trimmed - assert_range ranges, range(1, 2, 2, 14) + assert_range(ranges, range(1, 2, 2, 14)) # some: 123 - assert_range ranges, range(1, 2, 1, 11) + assert_range(ranges, range(1, 2, 1, 11)) # some - assert_range ranges, range(1, 2, 1, 6) + assert_range(ranges, range(1, 2, 1, 6)) end test "on alias" do @@ -471,13 +473,13 @@ defmodule ElixirLS.LanguageServer.Providers.SelectionRangesTest do ranges = get_ranges(text, 0, 2) # full range - assert_range ranges, range(0, 0, 4, 0) + assert_range(ranges, range(0, 0, 4, 0)) # full struct - assert_range ranges, range(0, 0, 3, 1) + assert_range(ranges, range(0, 0, 3, 1)) # %My.Struct - assert_range ranges, range(0, 0, 0, 10) + assert_range(ranges, range(0, 0, 0, 10)) # My.Struct - assert_range ranges, range(0, 1, 0, 10) + assert_range(ranges, range(0, 1, 0, 10)) end end @@ -490,15 +492,15 @@ defmodule ElixirLS.LanguageServer.Providers.SelectionRangesTest do ranges = get_ranges(text, 0, 6) # full range - assert_range ranges, range(0, 0, 1, 0) + assert_range(ranges, range(0, 0, 1, 0)) # full call - assert_range ranges, range(0, 0, 0, 46) + assert_range(ranges, range(0, 0, 0, 46)) # full () outside - assert_range ranges, range(0, 3, 0, 46) + assert_range(ranges, range(0, 3, 0, 46)) # full () inside - assert_range ranges, range(0, 4, 0, 45) + assert_range(ranges, range(0, 4, 0, 45)) # %My{} = my - assert_range ranges, range(0, 4, 0, 14) + assert_range(ranges, range(0, 4, 0, 14)) end test "between ," do @@ -509,15 +511,15 @@ defmodule ElixirLS.LanguageServer.Providers.SelectionRangesTest do ranges = get_ranges(text, 0, 18) # full range - assert_range ranges, range(0, 0, 1, 0) + assert_range(ranges, range(0, 0, 1, 0)) # full call - assert_range ranges, range(0, 0, 0, 46) + assert_range(ranges, range(0, 0, 0, 46)) # full () outside - assert_range ranges, range(0, 3, 0, 46) + assert_range(ranges, range(0, 3, 0, 46)) # full () inside - assert_range ranges, range(0, 4, 0, 45) + assert_range(ranges, range(0, 4, 0, 45)) # keyword: 123 - assert_range ranges, range(0, 16, 0, 28) + assert_range(ranges, range(0, 16, 0, 28)) end test "after last ," do @@ -528,15 +530,15 @@ defmodule ElixirLS.LanguageServer.Providers.SelectionRangesTest do ranges = get_ranges(text, 0, 31) # full range - assert_range ranges, range(0, 0, 1, 0) + assert_range(ranges, range(0, 0, 1, 0)) # full call - assert_range ranges, range(0, 0, 0, 46) + assert_range(ranges, range(0, 0, 0, 46)) # full () outside - assert_range ranges, range(0, 3, 0, 46) + assert_range(ranges, range(0, 3, 0, 46)) # full () inside - assert_range ranges, range(0, 4, 0, 45) + assert_range(ranges, range(0, 4, 0, 45)) # other: [:a, ""] - assert_range ranges, range(0, 30, 0, 45) + assert_range(ranges, range(0, 30, 0, 45)) end end @@ -551,17 +553,17 @@ defmodule ElixirLS.LanguageServer.Providers.SelectionRangesTest do funs() end """ - + ranges = get_ranges(text, 4, 5) - + # full range - assert_range ranges, range(0, 0, 7, 0) + assert_range(ranges, range(0, 0, 7, 0)) # full b case - assert_range ranges, range(3, 2, 5, 10) + assert_range(ranges, range(3, 2, 5, 10)) # b block - assert_range ranges, range(4, 4, 5, 10) + assert_range(ranges, range(4, 4, 5, 10)) # more() - assert_range ranges, range(4, 4, 4, 10) + assert_range(ranges, range(4, 4, 4, 10)) end test "inside case arg" do @@ -577,11 +579,11 @@ defmodule ElixirLS.LanguageServer.Providers.SelectionRangesTest do ranges = get_ranges(text, 0, 6) # full range - assert_range ranges, range(0, 0, 6, 0) + assert_range(ranges, range(0, 0, 6, 0)) # full case - assert_range ranges, range(0, 0, 5, 3) + assert_range(ranges, range(0, 0, 5, 3)) # foo - assert_range ranges, range(0, 5, 0, 8) + assert_range(ranges, range(0, 5, 0, 8)) end test "left side of -> single line" do @@ -597,19 +599,19 @@ defmodule ElixirLS.LanguageServer.Providers.SelectionRangesTest do ranges = get_ranges(text, 1, 3) # full range - assert_range ranges, range(0, 0, 6, 0) + assert_range(ranges, range(0, 0, 6, 0)) # full case - assert_range ranges, range(0, 0, 5, 3) + assert_range(ranges, range(0, 0, 5, 3)) # do block - assert_range ranges, range(0, 9, 5, 3) + assert_range(ranges, range(0, 9, 5, 3)) # do block inside - assert_range ranges, range(1, 0, 4, 10) + assert_range(ranges, range(1, 0, 4, 10)) # do block inside trimmed - assert_range ranges, range(1, 2, 4, 10) + assert_range(ranges, range(1, 2, 4, 10)) # full expression - assert_range ranges, range(1, 2, 1, 17) + assert_range(ranges, range(1, 2, 1, 17)) # {:ok, _} - assert_range ranges, range(1, 2, 1, 10) + assert_range(ranges, range(1, 2, 1, 10)) end test "right side of -> single line" do @@ -625,19 +627,19 @@ defmodule ElixirLS.LanguageServer.Providers.SelectionRangesTest do ranges = get_ranges(text, 1, 16) # full range - assert_range ranges, range(0, 0, 6, 0) + assert_range(ranges, range(0, 0, 6, 0)) # full case - assert_range ranges, range(0, 0, 5, 3) + assert_range(ranges, range(0, 0, 5, 3)) # do block - assert_range ranges, range(0, 9, 5, 3) + assert_range(ranges, range(0, 9, 5, 3)) # do block inside - assert_range ranges, range(1, 0, 4, 10) + assert_range(ranges, range(1, 0, 4, 10)) # do block inside trimmed - assert_range ranges, range(1, 2, 4, 10) + assert_range(ranges, range(1, 2, 4, 10)) # full expression - assert_range ranges, range(1, 2, 1, 17) + assert_range(ranges, range(1, 2, 1, 17)) # :ok expression - assert_range ranges, range(1, 14, 1, 17) + assert_range(ranges, range(1, 14, 1, 17)) end test "left side of -> multi line" do @@ -656,18 +658,17 @@ defmodule ElixirLS.LanguageServer.Providers.SelectionRangesTest do ranges = get_ranges(text, 3, 5) # full range - assert_range ranges, range(0, 0, 9, 0) + assert_range(ranges, range(0, 0, 9, 0)) # full case - assert_range ranges, range(0, 0, 8, 3) + assert_range(ranges, range(0, 0, 8, 3)) # do block - assert_range ranges, range(0, 9, 8, 3) - # TODO + assert_range(ranges, range(0, 9, 8, 3)) # case -> expression - assert_range ranges, range(2, 2, 6, 10) + assert_range(ranges, range(2, 2, 6, 10)) # pattern with -> - assert_range ranges, range(2, 2, 4, 6) + assert_range(ranges, range(2, 2, 4, 6)) # pattern - assert_range ranges, range(2, 2, 4, 3) + assert_range(ranges, range(2, 2, 4, 3)) end test "right side of -> multi line" do @@ -686,19 +687,19 @@ defmodule ElixirLS.LanguageServer.Providers.SelectionRangesTest do ranges = get_ranges(text, 5, 5) # full range - assert_range ranges, range(0, 0, 9, 0) + assert_range(ranges, range(0, 0, 9, 0)) # full case - assert_range ranges, range(0, 0, 8, 3) + assert_range(ranges, range(0, 0, 8, 3)) # do block - assert_range ranges, range(0, 9, 8, 3) + assert_range(ranges, range(0, 9, 8, 3)) # do block inside - assert_range ranges, range(1, 0, 7, 11) + assert_range(ranges, range(1, 0, 7, 11)) # do block inside trimmed - assert_range ranges, range(1, 2, 7, 11) + assert_range(ranges, range(1, 2, 7, 11)) # case -> expression - assert_range ranges, range(2, 2, 6, 10) + assert_range(ranges, range(2, 2, 6, 10)) # full block - assert_range ranges, range(5, 4, 6, 10) + assert_range(ranges, range(5, 4, 6, 10)) end test "right side of -> last expression in do block" do @@ -717,17 +718,17 @@ defmodule ElixirLS.LanguageServer.Providers.SelectionRangesTest do ranges = get_ranges(text, 7, 8) # full range - assert_range ranges, range(0, 0, 9, 0) + assert_range(ranges, range(0, 0, 9, 0)) # full case - assert_range ranges, range(0, 0, 8, 3) + assert_range(ranges, range(0, 0, 8, 3)) # do block - assert_range ranges, range(0, 9, 8, 3) + assert_range(ranges, range(0, 9, 8, 3)) # do block inside trimmed - assert_range ranges, range(1, 2, 7, 11) + assert_range(ranges, range(1, 2, 7, 11)) # case -> expression - assert_range ranges, range(7, 2, 7, 11) + assert_range(ranges, range(7, 2, 7, 11)) # :foo - assert_range ranges, range(7, 7, 7, 11) + assert_range(ranges, range(7, 7, 7, 11)) end end @@ -742,13 +743,13 @@ defmodule ElixirLS.LanguageServer.Providers.SelectionRangesTest do ranges = get_ranges(text, 1, 2) # full range - assert_range ranges, range(0, 0, 3, 0) + assert_range(ranges, range(0, 0, 3, 0)) # full for - assert_range ranges, range(0, 0, 2, 3) + assert_range(ranges, range(0, 0, 2, 3)) # do block - assert_range ranges, range(0, 35, 2, 3) + assert_range(ranges, range(0, 35, 2, 3)) # x + y expression - assert_range ranges, range(1, 2, 1, 7) + assert_range(ranges, range(1, 2, 1, 7)) end test "inside do expression single line" do @@ -759,11 +760,11 @@ defmodule ElixirLS.LanguageServer.Providers.SelectionRangesTest do ranges = get_ranges(text, 0, 51) # full range - assert_range ranges, range(0, 0, 1, 0) + assert_range(ranges, range(0, 0, 1, 0)) # full for - assert_range ranges, range(0, 0, 0, 56) + assert_range(ranges, range(0, 0, 0, 56)) # x + y expression - assert_range ranges, range(0, 51, 0, 56) + assert_range(ranges, range(0, 51, 0, 56)) end test "inside do expression" do @@ -776,12 +777,11 @@ defmodule ElixirLS.LanguageServer.Providers.SelectionRangesTest do ranges = get_ranges(text, 2, 6) # full range - assert_range ranges, range(0, 0, 3, 0) - # TODO - # It's difficult to get the full range of for expression as Inspect.Algebra formats it differently - # and there is no usable metadata + assert_range(ranges, range(0, 0, 3, 0)) + # full for expression + assert_range(ranges, range(0, 0, 2, 11)) # x + y expression - assert_range ranges, range(2, 6, 2, 11) + assert_range(ranges, range(2, 6, 2, 11)) end test "inside <- expression" do @@ -794,13 +794,148 @@ defmodule ElixirLS.LanguageServer.Providers.SelectionRangesTest do ranges = get_ranges(text, 0, 10) # full range - assert_range ranges, range(0, 0, 3, 0) + assert_range(ranges, range(0, 0, 3, 0)) # full for - assert_range ranges, range(0, 0, 2, 3) + assert_range(ranges, range(0, 0, 2, 3)) # x <- [1, 2, 3] - assert_range ranges, range(0, 4, 0, 18) + assert_range(ranges, range(0, 4, 0, 18)) # [1, 2, 3] - assert_range ranges, range(0, 9, 0, 18) + assert_range(ranges, range(0, 9, 0, 18)) + end + end + + describe "with" do + test "inside do block" do + text = """ + with x <- [1, 2, 3], y <- [4, 5, 6] do + x ++ y + end + """ + + ranges = get_ranges(text, 1, 2) + + # full range + assert_range(ranges, range(0, 0, 3, 0)) + # full for + assert_range(ranges, range(0, 0, 2, 3)) + # do block + assert_range(ranges, range(0, 36, 2, 3)) + # x ++ y expression + assert_range(ranges, range(1, 2, 1, 8)) + end + + test "inside do expression single line" do + text = """ + with x <- [1, 2, 3], y <- [4, 5, 6], do: x ++ y + """ + + ranges = get_ranges(text, 0, 51) + + # full range + assert_range(ranges, range(0, 0, 1, 0)) + # full for + assert_range(ranges, range(0, 0, 0, 47)) + # x ++ y expression + assert_range(ranges, range(0, 41, 0, 47)) + end + + test "inside do expression" do + text = """ + with x <- [1, 2, 3], + y <- [4, 5, 6], + do: x ++ y + """ + + ranges = get_ranges(text, 2, 6) + + # full range + assert_range(ranges, range(0, 0, 3, 0)) + # full for expression + assert_range(ranges, range(0, 0, 2, 12)) + # x ++ y expression + assert_range(ranges, range(2, 6, 2, 12)) + end + + test "inside <- expression" do + text = """ + with x <- [1, 2, 3], y <- [4, 5, 6] do + x ++ y + end + """ + + ranges = get_ranges(text, 0, 10) + + # full range + assert_range(ranges, range(0, 0, 3, 0)) + # full for + assert_range(ranges, range(0, 0, 2, 3)) + # x <- [1, 2, 3] + assert_range(ranges, range(0, 5, 0, 19)) + # [1, 2, 3] + assert_range(ranges, range(0, 10, 0, 19)) + end + end + + describe "if" do + test "inside condition" do + text = """ + if a + b > 1 do + :ok + else + :error + end + """ + + ranges = get_ranges(text, 0, 3) + + # full range + assert_range(ranges, range(0, 0, 5, 0)) + # full if + assert_range(ranges, range(0, 0, 4, 3)) + # condition + assert_range(ranges, range(0, 3, 0, 12)) + end + + test "inside do block" do + text = """ + if a + b > 1 do + :ok + else + :error + end + """ + + ranges = get_ranges(text, 1, 2) + + # full range + assert_range(ranges, range(0, 0, 5, 0)) + # full if + assert_range(ranges, range(0, 0, 4, 3)) + # do-else + assert_range(ranges, range(0, 15, 2, 0)) + # :ok + assert_range(ranges, range(1, 2, 1, 5)) + end + + test "inside else block" do + text = """ + if a + b > 1 do + :ok + else + :error + end + """ + + ranges = get_ranges(text, 3, 2) + + # full range + assert_range(ranges, range(0, 0, 5, 0)) + # full if + assert_range(ranges, range(0, 0, 4, 3)) + # else-end + assert_range(ranges, range(2, 0, 4, 3)) + # :error + assert_range(ranges, range(3, 2, 3, 8)) end end @@ -812,14 +947,14 @@ defmodule ElixirLS.LanguageServer.Providers.SelectionRangesTest do ranges = get_ranges(text, 0, 8) # full range - assert_range ranges, range(0, 0, 1, 0) + assert_range(ranges, range(0, 0, 1, 0)) # full expression - assert_range ranges, range(0, 0, 0, 32) + assert_range(ranges, range(0, 0, 0, 32)) # full left side of operator > - assert_range ranges, range(0, 0, 0, 18) + assert_range(ranges, range(0, 0, 0, 18)) # var2 * var3 - assert_range ranges, range(0, 7, 0, 18) + assert_range(ranges, range(0, 7, 0, 18)) # var2 - assert_range ranges, range(0, 7, 0, 11) + assert_range(ranges, range(0, 7, 0, 11)) end end