From d17d22ff9aa4d2ae4fe4dd610efa10dd758f3635 Mon Sep 17 00:00:00 2001 From: William Lanchantin Date: Tue, 9 Feb 2021 15:54:50 -0500 Subject: [PATCH 01/61] initial commit --- .../lib/language_server/protocol.ex | 10 ++ .../providers/folding_range.ex | 147 ++++++++++++++++++ .../lib/language_server/server.ex | 17 +- .../test/providers/folding_range_test.exs | 101 ++++++++++++ 4 files changed, 273 insertions(+), 2 deletions(-) create mode 100644 apps/language_server/lib/language_server/providers/folding_range.ex create mode 100644 apps/language_server/test/providers/folding_range_test.exs diff --git a/apps/language_server/lib/language_server/protocol.ex b/apps/language_server/lib/language_server/protocol.ex index a61ed23f5..8ef38f094 100644 --- a/apps/language_server/lib/language_server/protocol.ex +++ b/apps/language_server/lib/language_server/protocol.ex @@ -190,6 +190,16 @@ defmodule ElixirLS.LanguageServer.Protocol do end end + defmacro folding_range_req(id, uri) do + quote do + request(unquote(id), "textDocument/foldingRange", %{ + "textDocument" => %{ + "uri" => unquote(uri) + } + }) + end + end + defmacro macro_expansion(id, whole_buffer, selected_macro, macro_line) do quote do request(unquote(id), "elixirDocument/macroExpansion", %{ diff --git a/apps/language_server/lib/language_server/providers/folding_range.ex b/apps/language_server/lib/language_server/providers/folding_range.ex new file mode 100644 index 000000000..f36321bcf --- /dev/null +++ b/apps/language_server/lib/language_server/providers/folding_range.ex @@ -0,0 +1,147 @@ +defmodule ElixirLS.LanguageServer.Providers.FoldingRange do + @moduledoc """ + A textDocument/foldingRange provider implementation. + + See specification here: + https://microsoft.github.io/language-server-protocol/specifications/specification-3-15/#textDocument_foldingRange + """ + + alias ElixirSense.Core.Normalized.Tokenizer + + @range_pairs %{ + do: :end, + "(": :")", + "[": :"]", + "{": :"}", + bin_heredoc: :eol + } + + @doc """ + Provides folding ranges for a source file + + ## Example + + text = \"\"\" + defmodule A do # 0 + def hello() do # 1 + :world # 2 + end # 3 + end # 4 + \"\"\" + + {:ok, ranges} = FoldingRange.provide(%{text: text}) + + ranges + # [ + # %{"startLine" => 0, "endLine" => 3}, + # %{"startLine" => 1, "endLine" => 2} + # ] + """ + @spec provide(%{text: String.t()}) :: + {:ok, [%{required(String.t()) => non_neg_integer()}]} | {:error, String.t()} + def provide(%{text: text}) do + do_provide(text) + end + + def provide(not_a_source_file) do + {:error, "Expected a source file, found: #{inspect(not_a_source_file)}"} + end + + defp do_provide(text) do + ranges = + text + |> Tokenizer.tokenize() + |> format_tokens() + |> case do + {:ok, formatted_tokens} -> formatted_tokens |> fold_tokens_into_ranges() + _ -> [] + end + + {:ok, ranges} + end + + # Make pattern-matching easier by forcing all tuples to be 3-tuples + defp format_tokens(reversed_tokens) when is_list(reversed_tokens) do + reversed_tokens + # This reverses the tokens, but they come out of Tokenizer.tokenize/1 + # already reversed. + |> Enum.reduce_while({:ok, []}, fn tuple, {:ok, acc} -> + tuple = + case tuple do + {a, b} -> {a, b, nil} + {a, b, c} -> {a, b, c} + # raise here? + _ -> :error + end + + if tuple == :error do + {:halt, :error} + else + {:cont, {:ok, [tuple | acc]}} + end + end) + end + + # Note + # This implementation allows for the possibility of 2 ranges with the same + # startLines but different endLines. + # It's not clear if that case is actually a problem. + defp fold_tokens_into_ranges(tokens) when is_list(tokens) do + tokens + |> pair_tokens([], []) + |> Enum.map(fn {{_, {start_line, _, _}, _}, {_, {end_line, _, _}, _}} -> + # -1 for both because the server expects 0-indexing + # Another -1 for end_line because the range should stop 1 short + # e.g. both "do" and "end" should be visible when collapsed + {start_line - 1, end_line - 2} + end) + |> Enum.filter(fn {start_line, end_line} -> end_line > start_line end) + |> Enum.sort() + |> Enum.dedup() + ## Remove the above sort + dedup lines and uncomment the following if no + ## two ranges may share a startLine + # |> Enum.group_by(fn {start_line, _} -> start_line end) + # |> Enum.map(fn {_, ranges} -> + # Enum.max_by(ranges, fn {_, end_line} -> end_line end) + # end) + |> Enum.map(fn {start_line, end_line} -> + %{"startLine" => start_line, "endLine" => end_line} + end) + end + + # A stack-based approach to match range pairs + # Notes + # - The returned pairs will be ordered by the line of the 2nd element. + # - Tokenizer.tokenize/1 doesn't differentiate between successful and failed + # attempts to tokenize the string. + # This could mean the returned tokens are unbalaned. + # Therefore, the stack may not be empty when the base clause is hit. + # We're choosing to return the successfully paired tokens rather than to + # return an error if not all tokens could be paired. + defp pair_tokens([], _stack, pairs), do: pairs + + defp pair_tokens([{start_kind, _, _} = start | tail_tokens], [], pairs) do + new_stack = if Map.get(@range_pairs, start_kind), do: [start], else: [] + pair_tokens(tail_tokens, new_stack, pairs) + end + + defp pair_tokens( + [{start_kind, _, _} = start | tail_tokens], + [{top_kind, _, _} = top | tail_stack] = stack, + pairs + ) do + {new_stack, new_pairs} = + cond do + Map.get(@range_pairs, top_kind) == start_kind -> + {tail_stack, [{top, start} | pairs]} + + Map.get(@range_pairs, start_kind) -> + {[start | stack], pairs} + + true -> + {stack, pairs} + end + + pair_tokens(tail_tokens, new_stack, new_pairs) + end +end diff --git a/apps/language_server/lib/language_server/server.ex b/apps/language_server/lib/language_server/server.ex index fea5d443b..28822d1c4 100644 --- a/apps/language_server/lib/language_server/server.ex +++ b/apps/language_server/lib/language_server/server.ex @@ -30,7 +30,8 @@ defmodule ElixirLS.LanguageServer.Server do WorkspaceSymbols, OnTypeFormatting, CodeLens, - ExecuteCommand + ExecuteCommand, + FoldingRange } alias ElixirLS.Utils.Launch @@ -729,6 +730,17 @@ defmodule ElixirLS.LanguageServer.Server do end, state} end + defp handle_request(folding_range_req(_id, uri), state) do + case state.source_files[uri] do + nil -> + {:error, :server_error, "Missing source file", state} + + source_file -> + fun = fn -> FoldingRange.provide(source_file) end + {:async, fun, state} + end + end + defp handle_request(macro_expansion(_id, whole_buffer, selected_macro, macro_line), state) do x = ElixirSense.expand_full(whole_buffer, selected_macro, macro_line) {:ok, x, state} @@ -782,7 +794,8 @@ defmodule ElixirLS.LanguageServer.Server do "executeCommandProvider" => %{"commands" => ["spec:#{server_instance_id}"]}, "workspace" => %{ "workspaceFolders" => %{"supported" => false, "changeNotifications" => false} - } + }, + "foldingRangeProvider" => true } end diff --git a/apps/language_server/test/providers/folding_range_test.exs b/apps/language_server/test/providers/folding_range_test.exs new file mode 100644 index 000000000..8b5e8185d --- /dev/null +++ b/apps/language_server/test/providers/folding_range_test.exs @@ -0,0 +1,101 @@ +defmodule ElixirLS.LanguageServer.Providers.FoldingRangeTest do + use ExUnit.Case + + alias ElixirLS.LanguageServer.Providers.FoldingRange + + test "returns an :error tuple if input is not a source file" do + assert {:error, _} = %{} |> FoldingRange.provide() + end + + describe "genuine source files" do + setup [:fold_text] + + @tag text: """ + defmodule A do # 0 + def hello() do # 1 + :world # 2 + end # 3 + end # 4 + """ + test "can fold 1 defmodule, 1 def", %{ranges_result: ranges_result} do + assert {:ok, ranges} = ranges_result + assert compare_condensed_ranges(ranges, [{0, 3}, {1, 2}]) + end + + @tag text: """ + defmodule A do # 0 + def hello() do # 1 + \"\"\" + hello # 3 + \"\"\" + end # 5 + end # 6 + """ + test "can fold 1 defmodule, 1 def, 1 heredoc", %{ranges_result: ranges_result} do + assert {:ok, ranges} = ranges_result + assert compare_condensed_ranges(ranges, [{0, 5}, {1, 4}, {2, 3}]) + end + + @tag text: """ + defmodule A do # 0 + def hello() do # 1 + a = 20 # 2 + # 3 + case a do # 4 + 20 -> :ok # 5 + _ -> :error # 6 + end # 7 + end # 8 + end # 9 + """ + test "can fold 1 defmodule, 1 complex def", %{ranges_result: ranges_result} do + assert {:ok, ranges} = ranges_result + assert compare_condensed_ranges(ranges, [{0, 8}, {1, 7}, {4, 6}]) + end + + @tag text: """ + defmodule A do # 0 + @moduledoc "This is module A" # 1 + end # 2 + # 3 + defmodule B do # 4 + @moduledoc "This is module B" # 5 + end # 6 + """ + test "can fold 2 defmodules in the top-level of file", %{ranges_result: ranges_result} do + assert {:ok, ranges} = ranges_result + assert compare_condensed_ranges(ranges, [{0, 1}, {4, 5}]) + end + + @tag text: """ + defmodule A do # 0 + def compare_and_hello(list) do # 1 + assert list == [ # 2 + %{"a" => 1, "b" => 2}, # 3 + %{"a" => 3, "b" => 4}, # 4 + ] # 5 + # 6 + :world # 7 + end # 8 + end # 9 + """ + test "can fold 1 defmodule, 1 def, 1 list", %{ranges_result: ranges_result} do + assert {:ok, ranges} = ranges_result + assert compare_condensed_ranges(ranges, [{0, 8}, {1, 7}, {2, 4}]) + end + end + + defp fold_text(%{text: text} = context) do + ranges_result = %{text: text} |> FoldingRange.provide() + {:ok, Map.put(context, :ranges_result, ranges_result)} + end + + defp compare_condensed_ranges(result, condensed_expected) do + condensed_result = result |> Enum.map(&condense_range/1) + assert condensed_result == condensed_expected + end + + defp condense_range(range) do + {range["startLine"], range["endLine"]} + end +end From 01c966d5417d16986234f2be3a45aeed3029c89b Mon Sep 17 00:00:00 2001 From: William Lanchantin Date: Tue, 9 Feb 2021 17:04:43 -0500 Subject: [PATCH 02/61] can fold heredoc w/ closing paren --- .../providers/folding_range.ex | 52 ++++++++++++------- .../test/providers/folding_range_test.exs | 29 +++++++++++ 2 files changed, 62 insertions(+), 19 deletions(-) diff --git a/apps/language_server/lib/language_server/providers/folding_range.ex b/apps/language_server/lib/language_server/providers/folding_range.ex index f36321bcf..4cbde0b29 100644 --- a/apps/language_server/lib/language_server/providers/folding_range.ex +++ b/apps/language_server/lib/language_server/providers/folding_range.ex @@ -8,11 +8,13 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRange do alias ElixirSense.Core.Normalized.Tokenizer - @range_pairs %{ + @basic_pairs %{ do: :end, "(": :")", "[": :"]", - "{": :"}", + "{": :"}" + } + @heredoc_pairs %{ bin_heredoc: :eol } @@ -87,14 +89,14 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRange do # startLines but different endLines. # It's not clear if that case is actually a problem. defp fold_tokens_into_ranges(tokens) when is_list(tokens) do - tokens - |> pair_tokens([], []) - |> Enum.map(fn {{_, {start_line, _, _}, _}, {_, {end_line, _, _}, _}} -> - # -1 for both because the server expects 0-indexing - # Another -1 for end_line because the range should stop 1 short - # e.g. both "do" and "end" should be visible when collapsed - {start_line - 1, end_line - 2} - end) + ranges_from_pairs = tokens |> pair_tokens(@basic_pairs) + ranges_from_heredocs = tokens |> pair_tokens(@heredoc_pairs) + ranges = ranges_from_pairs ++ ranges_from_heredocs + ranges |> convert_to_spec_ranges() + end + + defp convert_to_spec_ranges(ranges) do + ranges |> Enum.filter(fn {start_line, end_line} -> end_line > start_line end) |> Enum.sort() |> Enum.dedup() @@ -109,6 +111,17 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRange do end) end + defp pair_tokens(tokens, kind_map) do + tokens + |> do_pair_tokens([], [], kind_map) + |> Enum.map(fn {{_, {start_line, _, _}, _}, {_, {end_line, _, _}, _}} -> + # -1 for both because the server expects 0-indexing + # Another -1 for end_line because the range should stop 1 short + # e.g. both "do" and "end" should be visible when collapsed + {start_line - 1, end_line - 2} + end) + end + # A stack-based approach to match range pairs # Notes # - The returned pairs will be ordered by the line of the 2nd element. @@ -118,30 +131,31 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRange do # Therefore, the stack may not be empty when the base clause is hit. # We're choosing to return the successfully paired tokens rather than to # return an error if not all tokens could be paired. - defp pair_tokens([], _stack, pairs), do: pairs + defp do_pair_tokens([], _stack, pairs, _kind_map), do: pairs - defp pair_tokens([{start_kind, _, _} = start | tail_tokens], [], pairs) do - new_stack = if Map.get(@range_pairs, start_kind), do: [start], else: [] - pair_tokens(tail_tokens, new_stack, pairs) + defp do_pair_tokens([{start_kind, _, _} = start | tail_tokens], [], pairs, kind_map) do + new_stack = if Map.get(kind_map, start_kind), do: [start], else: [] + do_pair_tokens(tail_tokens, new_stack, pairs, kind_map) end - defp pair_tokens( + defp do_pair_tokens( [{start_kind, _, _} = start | tail_tokens], [{top_kind, _, _} = top | tail_stack] = stack, - pairs + pairs, + kind_map ) do {new_stack, new_pairs} = cond do - Map.get(@range_pairs, top_kind) == start_kind -> + Map.get(kind_map, top_kind) == start_kind -> {tail_stack, [{top, start} | pairs]} - Map.get(@range_pairs, start_kind) -> + Map.get(kind_map, start_kind) -> {[start | stack], pairs} true -> {stack, pairs} end - pair_tokens(tail_tokens, new_stack, new_pairs) + do_pair_tokens(tail_tokens, new_stack, new_pairs, kind_map) end end diff --git a/apps/language_server/test/providers/folding_range_test.exs b/apps/language_server/test/providers/folding_range_test.exs index 8b5e8185d..34571a43c 100644 --- a/apps/language_server/test/providers/folding_range_test.exs +++ b/apps/language_server/test/providers/folding_range_test.exs @@ -83,6 +83,35 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRangeTest do assert {:ok, ranges} = ranges_result assert compare_condensed_ranges(ranges, [{0, 8}, {1, 7}, {2, 4}]) end + + @tag text: """ + defmodule A do # 0 + def f(%{"key" => value} = map) do # 1 + case NaiveDateTime.from_iso8601(value) do # 2 + {:ok, ndt} -> # 3 + dt = # 4 + ndt # 5 + |> DateTime.from_naive!("Etc/UTC") # 6 + |> Map.put(:microsecond, {0, 6}) # 7 + # 8 + %{map | "key" => dt} # 9 + # 10 + e -> # 11 + Logger.warn(\"\"\" + Could not use data map from #\{inspect(value)\} # 13 + #\{inspect(e)\} # 14 + \"\"\") + # 16 + :could_not_parse_value # 17 + end # 18 + end # 19 + end # 20 + """ + test "can fold heredoc w/ closing paren", %{ranges_result: ranges_result} do + assert {:ok, ranges} = ranges_result + ranges |> IO.inspect() + # assert compare_condensed_ranges(ranges, [{0, 8}, {1, 7}, {2, 4}]) + end end defp fold_text(%{text: text} = context) do From 64752ddc527c90d1a8625bf2e60cec09b01b2b21 Mon Sep 17 00:00:00 2001 From: William Lanchantin Date: Sat, 13 Feb 2021 11:59:52 -0500 Subject: [PATCH 03/61] fix indentation --- .../test/providers/folding_range_test.exs | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/apps/language_server/test/providers/folding_range_test.exs b/apps/language_server/test/providers/folding_range_test.exs index 34571a43c..4a814127e 100644 --- a/apps/language_server/test/providers/folding_range_test.exs +++ b/apps/language_server/test/providers/folding_range_test.exs @@ -85,27 +85,27 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRangeTest do end @tag text: """ - defmodule A do # 0 - def f(%{"key" => value} = map) do # 1 - case NaiveDateTime.from_iso8601(value) do # 2 - {:ok, ndt} -> # 3 - dt = # 4 - ndt # 5 - |> DateTime.from_naive!("Etc/UTC") # 6 - |> Map.put(:microsecond, {0, 6}) # 7 - # 8 - %{map | "key" => dt} # 9 - # 10 - e -> # 11 + defmodule A do # 0 + def f(%{"key" => value} = map) do # 1 + case NaiveDateTime.from_iso8601(value) do # 2 + {:ok, ndt} -> # 3 + dt = # 4 + ndt # 5 + |> DateTime.from_naive!("Etc/UTC") # 6 + |> Map.put(:microsecond, {0, 6}) # 7 + # 8 + %{map | "key" => dt} # 9 + # 10 + e -> # 11 Logger.warn(\"\"\" - Could not use data map from #\{inspect(value)\} # 13 - #\{inspect(e)\} # 14 + Could not use data map from #\{inspect(value)\} # 13 + #\{inspect(e)\} # 14 \"\"\") - # 16 - :could_not_parse_value # 17 - end # 18 - end # 19 - end # 20 + # 16 + :could_not_parse_value # 17 + end # 18 + end # 19 + end # 20 """ test "can fold heredoc w/ closing paren", %{ranges_result: ranges_result} do assert {:ok, ranges} = ranges_result From 3e40c30924b4082e0cb1f04a4e1f76c1358822b5 Mon Sep 17 00:00:00 2001 From: William Lanchantin Date: Mon, 15 Feb 2021 11:49:29 -0500 Subject: [PATCH 04/61] save progress (this is a mess...) --- .../providers/folding_range.ex | 130 +++++++++++++++++- .../test/providers/folding_range_test.exs | 71 +++++----- 2 files changed, 167 insertions(+), 34 deletions(-) diff --git a/apps/language_server/lib/language_server/providers/folding_range.ex b/apps/language_server/lib/language_server/providers/folding_range.ex index 4cbde0b29..c1d1a56c0 100644 --- a/apps/language_server/lib/language_server/providers/folding_range.ex +++ b/apps/language_server/lib/language_server/providers/folding_range.ex @@ -4,6 +4,11 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRange do See specification here: https://microsoft.github.io/language-server-protocol/specifications/specification-3-15/#textDocument_foldingRange + + ## TODO + + - [ ] Indentation pass + - [ ] Add priorities and do a proper merge """ alias ElixirSense.Core.Normalized.Tokenizer @@ -50,7 +55,7 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRange do end defp do_provide(text) do - ranges = + _ranges = text |> Tokenizer.tokenize() |> format_tokens() @@ -59,6 +64,8 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRange do _ -> [] end + ranges = text |> from_indentation() + {:ok, ranges} end @@ -158,4 +165,125 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRange do do_pair_tokens(tail_tokens, new_stack, new_pairs, kind_map) end + + defp from_indentation(text) do + cells = + text + |> String.split("\n") + |> Enum.with_index() + |> Enum.map(fn {line, row} -> + full = line |> String.length() + trimmed = line |> String.trim_leading() |> String.length() + col = if {full, trimmed} == {0, 0}, do: nil, else: full - trimmed + 1 + {row, col} + end) + + # |> Enum.chunk_by(fn {_, col} -> col end) + # |> Enum.map(fn + # [x] -> x + # list -> list |> List.last() + # end) + # |> IO.inspect() + cells + # |> pair_cells([], []) + |> pair_cells_nsq([]) + # |> Enum.sort() + |> IO.inspect() + |> collapse_nil(cells) + |> IO.inspect() + |> Enum.reject(fn {{r1, _}, {r2, _}} -> r1 >= r2 end) + |> IO.inspect() + |> Enum.map(fn {{start_line, _}, {end_line, _}} -> + %{"startLine" => start_line, "endLine" => end_line} + end) + end + + # defp pair_cells([], _, pairs), do: pairs + + # defp pair_cells([cell | rest], [], pairs) do + # pair_cells(rest, [cell], pairs) + # end + + # defp pair_cells( + # [{row_cur, col_cur} = cur | rest], + # [{_, col_top} = top | tail_stack] = stack, + # pairs + # ) do + # # cur |> IO.inspect(label: :current) + # # top |> IO.inspect(label: :top_stack) + # # tail_stack |> IO.inspect(label: :tail_stack) + # # pairs |> IO.inspect(label: :pairs) + # # "" |> IO.puts() + + # {new_stack, new_pairs} = + # cond do + # is_nil(col_cur) -> + # case stack do + # [right | [left | new_tail_stack]] -> + # {new_tail_stack, [{left, right} | pairs]} + + # _ -> + # {tail_stack, pairs} + # end + + # col_cur > col_top -> + # {[cur | stack], pairs} + + # col_cur == col_top -> + # {tail_stack, [{top, {row_cur - 1, col_cur}} | pairs]} + + # col_cur < col_top -> + # case tail_stack do + # [match | new_tail_stack] -> + # {new_tail_stack, [{match, {row_cur - 1, col_cur}} | pairs]} + + # _ -> + # {tail_stack, pairs} + # end + # end + + # pair_cells(rest, new_stack, new_pairs) + # end + + defp pair_cells_nsq([], pairs) do + pairs + |> IO.inspect() + |> Enum.reduce([], fn {{_r1, c1}, {_r2, c2}} = pair, pairs -> + if c1 > c2 do + pairs + else + [pair | pairs] + end + end) + end + + defp pair_cells_nsq([{_, nil} | tail], pairs) do + pair_cells_nsq(tail, pairs) + end + + defp pair_cells_nsq([{_, ch} = head | tail], pairs) do + first_leq = Enum.find(tail, fn {_, ct} -> ct <= ch end) + # head |> IO.inspect(label: :head) + # first_leq |> IO.inspect(label: :first_leq) + # pairs |> IO.inspect(label: :pairs) + # "" |> IO.puts() + + new_pairs = + case first_leq do + nil -> pairs + first_leq -> [{head, first_leq} | pairs] + end + + pair_cells_nsq(tail, new_pairs) + end + + defp collapse_nil(pairs, cells) do + search = cells |> Enum.reverse() + + pairs + |> Enum.map(fn {cell1, {r2, _}} -> + cell2 = Enum.find(search, fn {r, c} -> r <= r2 and !is_nil(c) end) + {cell1, cell2} + end) + end end diff --git a/apps/language_server/test/providers/folding_range_test.exs b/apps/language_server/test/providers/folding_range_test.exs index 4a814127e..f6fd2e139 100644 --- a/apps/language_server/test/providers/folding_range_test.exs +++ b/apps/language_server/test/providers/folding_range_test.exs @@ -18,8 +18,8 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRangeTest do end # 4 """ test "can fold 1 defmodule, 1 def", %{ranges_result: ranges_result} do - assert {:ok, ranges} = ranges_result - assert compare_condensed_ranges(ranges, [{0, 3}, {1, 2}]) + assert {:ok, _ranges} = ranges_result + # assert compare_condensed_ranges(ranges, [{0, 3}, {1, 2}]) end @tag text: """ @@ -32,39 +32,42 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRangeTest do end # 6 """ test "can fold 1 defmodule, 1 def, 1 heredoc", %{ranges_result: ranges_result} do - assert {:ok, ranges} = ranges_result - assert compare_condensed_ranges(ranges, [{0, 5}, {1, 4}, {2, 3}]) + assert {:ok, _ranges} = ranges_result + # assert compare_condensed_ranges(ranges, [{0, 5}, {1, 4}, {2, 3}]) end @tag text: """ defmodule A do # 0 def hello() do # 1 a = 20 # 2 - # 3 + case a do # 4 - 20 -> :ok # 5 - _ -> :error # 6 - end # 7 - end # 8 - end # 9 + 20 -> # 5 + :ok # 6 + + _ -> # 8 + :error # 9 + end # 10 + end # 11 + end # 12 """ test "can fold 1 defmodule, 1 complex def", %{ranges_result: ranges_result} do - assert {:ok, ranges} = ranges_result - assert compare_condensed_ranges(ranges, [{0, 8}, {1, 7}, {4, 6}]) + assert {:ok, _ranges} = ranges_result + # assert compare_condensed_ranges(ranges, [{0, 8}, {1, 7}, {4, 6}]) end @tag text: """ defmodule A do # 0 @moduledoc "This is module A" # 1 end # 2 - # 3 + defmodule B do # 4 @moduledoc "This is module B" # 5 end # 6 """ test "can fold 2 defmodules in the top-level of file", %{ranges_result: ranges_result} do - assert {:ok, ranges} = ranges_result - assert compare_condensed_ranges(ranges, [{0, 1}, {4, 5}]) + assert {:ok, _ranges} = ranges_result + # assert compare_condensed_ranges(ranges, [{0, 1}, {4, 5}]) end @tag text: """ @@ -74,14 +77,14 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRangeTest do %{"a" => 1, "b" => 2}, # 3 %{"a" => 3, "b" => 4}, # 4 ] # 5 - # 6 + :world # 7 end # 8 end # 9 """ test "can fold 1 defmodule, 1 def, 1 list", %{ranges_result: ranges_result} do - assert {:ok, ranges} = ranges_result - assert compare_condensed_ranges(ranges, [{0, 8}, {1, 7}, {2, 4}]) + assert {:ok, _ranges} = ranges_result + # assert compare_condensed_ranges(ranges, [{0, 8}, {1, 7}, {2, 4}]) end @tag text: """ @@ -90,41 +93,43 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRangeTest do case NaiveDateTime.from_iso8601(value) do # 2 {:ok, ndt} -> # 3 dt = # 4 - ndt # 5 - |> DateTime.from_naive!("Etc/UTC") # 6 - |> Map.put(:microsecond, {0, 6}) # 7 - # 8 + ndt # 5 + |> DateTime.from_naive!("Etc/UTC") # 6 + |> Map.put(:microsecond, {0, 6}) # 7 + %{map | "key" => dt} # 9 - # 10 + e -> # 11 Logger.warn(\"\"\" Could not use data map from #\{inspect(value)\} # 13 #\{inspect(e)\} # 14 \"\"\") - # 16 + :could_not_parse_value # 17 end # 18 end # 19 end # 20 """ test "can fold heredoc w/ closing paren", %{ranges_result: ranges_result} do - assert {:ok, ranges} = ranges_result - ranges |> IO.inspect() + assert {:ok, _ranges} = ranges_result + # ranges |> IO.inspect() # assert compare_condensed_ranges(ranges, [{0, 8}, {1, 7}, {2, 4}]) end end defp fold_text(%{text: text} = context) do + "" |> IO.puts() + text |> IO.puts() ranges_result = %{text: text} |> FoldingRange.provide() {:ok, Map.put(context, :ranges_result, ranges_result)} end - defp compare_condensed_ranges(result, condensed_expected) do - condensed_result = result |> Enum.map(&condense_range/1) - assert condensed_result == condensed_expected - end + # defp compare_condensed_ranges(result, condensed_expected) do + # condensed_result = result |> Enum.map(&condense_range/1) + # assert condensed_result == condensed_expected + # end - defp condense_range(range) do - {range["startLine"], range["endLine"]} - end + # defp condense_range(range) do + # {range["startLine"], range["endLine"]} + # end end From 56fd54f5aa5f8cee97badfeb868e84cf786c2317 Mon Sep 17 00:00:00 2001 From: William Lanchantin Date: Mon, 15 Feb 2021 14:12:05 -0500 Subject: [PATCH 05/61] (potentially?) working version --- .../providers/folding_range/indentation.ex | 190 ++++++++++++++++++ .../test/providers/folding_range_test.exs | 105 ++++++++++ 2 files changed, 295 insertions(+) create mode 100644 apps/language_server/lib/language_server/providers/folding_range/indentation.ex diff --git a/apps/language_server/lib/language_server/providers/folding_range/indentation.ex b/apps/language_server/lib/language_server/providers/folding_range/indentation.ex new file mode 100644 index 000000000..ba1fd9152 --- /dev/null +++ b/apps/language_server/lib/language_server/providers/folding_range/indentation.ex @@ -0,0 +1,190 @@ +defmodule ElixirLS.LanguageServer.Providers.FoldingRange.Indentation do + @moduledoc """ + Code folding based on indentation only. + """ + + @doc """ + """ + def provide_ranges(text) do + cells = find_cells(text) |> IO.inspect(label: :cells) + ranges = pair_cells(cells, [], []) + ranges_2 = pair_cells_2(cells) + ranges_2 |> IO.inspect(label: :ranges_2) + {:ok, ranges} + end + + # If we think of the code text as a grid, this function finds the cells whose + # columns are the start of each row (line). + # Empty rows are represented as {row, nil}. + @spec find_cells(String.t()) :: [{non_neg_integer(), non_neg_integer()}] + defp find_cells(text) do + text + |> String.trim() + |> String.split("\n") + |> Enum.with_index() + |> Enum.map(fn {line, row} -> + full = line |> String.length() + trimmed = line |> String.trim_leading() |> String.length() + col = if {full, trimmed} == {0, 0}, do: nil, else: full - trimmed + {row, col} + end) + end + + defp pair_cells([], _, pairs), do: pairs + + defp pair_cells([cell | rest], [], pairs) do + pair_cells(rest, [cell], pairs) + end + + defp pair_cells( + [{row_cur, col_cur} = cur | rest], + [{_, col_top} = top | tail_stack] = stack, + pairs + ) do + # cur |> IO.inspect(label: :current) + # top |> IO.inspect(label: :top_stack) + # tail_stack |> IO.inspect(label: :tail_stack) + # pairs |> IO.inspect(label: :pairs) + # "" |> IO.puts() + + {new_stack, new_pairs} = + cond do + is_nil(col_cur) -> + case stack do + [right | [left | new_tail_stack]] -> + {new_tail_stack, [{left, right} | pairs]} + + _ -> + {tail_stack, pairs} + end + + col_cur > col_top -> + {[cur | stack], pairs} + + col_cur == col_top -> + {tail_stack, [{top, {row_cur - 1, col_cur}} | pairs]} + + col_cur < col_top -> + case tail_stack do + [match | new_tail_stack] -> + {new_tail_stack, [{match, {row_cur - 1, col_cur}} | pairs]} + + _ -> + {tail_stack, pairs} + end + end + + pair_cells(rest, new_stack, new_pairs) + end + + def pair_cells_2(cells) do + do_pair_cells_2(cells, [], [], []) + end + + defp do_pair_cells_2([], _, _, pairs) do + pairs + |> Enum.map(fn + {cell1, cell2, []} -> + {cell1, cell2} + + {cell1, _, empties} -> + [{first_empty_row, _} | _] = empties |> Enum.reverse() + {cell1, {first_empty_row - 1, nil}} + end) + |> Enum.reject(fn {{r1, _}, {r2, _}} -> r2 <= r1 + 1 end) + |> Enum.sort() + end + + defp do_pair_cells_2([{_, col} = cell | tail], [], empties, pairs) do + {new_stack, new_empties} = + if is_nil(col) do + {[], [cell | empties]} + else + {[cell], empties} + end + + do_pair_cells_2(tail, new_stack, new_empties, pairs) + end + + defp do_pair_cells_2( + [{_, col_cur} = cur | tail_cells], + [{_, col_top} = top | tail_stack] = stack, + empties, + pairs + ) do + cur |> IO.inspect(label: :cur) + stack |> IO.inspect(label: :stack) + empties |> IO.inspect(label: :empties) + pairs |> IO.inspect(label: :pairs) + + {new_stack, new_empties, new_pairs} = + cond do + is_nil(col_cur) -> + "is_nil" |> IO.inspect(label: :clause) + {stack, [cur | empties], pairs} + + col_cur > col_top -> + ">" |> IO.inspect(label: :clause) + {[cur | stack], [], pairs} + + col_cur == col_top -> + "==" |> IO.inspect(label: :clause) + {[cur | tail_stack], [], [{top, cur, empties} | pairs]} + + col_cur < col_top -> + "<" |> IO.inspect(label: :clause) + gr? = fn {_, c} -> col_cur <= c end + {leftovers, new_tail_stack} = stack |> Enum.split_while(gr?) + stack |> Enum.split_while(gr?) |> IO.inspect(label: :lists) + leftovers |> IO.inspect() + new_tail_stack |> IO.inspect(label: :new_tail_stack) + new_pairs = leftovers |> Enum.map(&{&1, cur, empties}) + {new_tail_stack, [], new_pairs ++ pairs} + end + + "" |> IO.puts() + do_pair_cells_2(tail_cells, new_stack, new_empties, new_pairs) + end + + # defp pair_cells_nsq([], pairs) do + # pairs + # |> IO.inspect() + # |> Enum.reduce([], fn {{_r1, c1}, {_r2, c2}} = pair, pairs -> + # if c1 > c2 do + # pairs + # else + # [pair | pairs] + # end + # end) + # end + + # defp pair_cells_nsq([{_, nil} | tail], pairs) do + # pair_cells_nsq(tail, pairs) + # end + + # defp pair_cells_nsq([{_, ch} = head | tail], pairs) do + # first_leq = Enum.find(tail, fn {_, ct} -> ct <= ch end) + # # head |> IO.inspect(label: :head) + # # first_leq |> IO.inspect(label: :first_leq) + # # pairs |> IO.inspect(label: :pairs) + # # "" |> IO.puts() + + # new_pairs = + # case first_leq do + # nil -> pairs + # first_leq -> [{head, first_leq} | pairs] + # end + + # pair_cells_nsq(tail, new_pairs) + # end + + # defp collapse_nil(pairs, cells) do + # search = cells |> Enum.reverse() + + # pairs + # |> Enum.map(fn {cell1, {r2, _}} -> + # cell2 = Enum.find(search, fn {r, c} -> r <= r2 and !is_nil(c) end) + # {cell1, cell2} + # end) + # end +end diff --git a/apps/language_server/test/providers/folding_range_test.exs b/apps/language_server/test/providers/folding_range_test.exs index f6fd2e139..11a6de75a 100644 --- a/apps/language_server/test/providers/folding_range_test.exs +++ b/apps/language_server/test/providers/folding_range_test.exs @@ -7,6 +7,111 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRangeTest do assert {:error, _} = %{} |> FoldingRange.provide() end + describe "indentation" do + setup [:fold_cells] + + # defmodule A do # 0 + # def hello() do # 1 + # :world # 2 + # end # 3 + # end # 4 + @tag cells: [{0, 0}, {1, 2}, {2, 4}, {3, 2}, {4, 0}] + test "basic indentation test", %{pairs: pairs} do + assert pairs == [{{0, 0}, {4, 0}}, {{1, 2}, {3, 2}}] + end + + # defmodule A do # 0 + # def hello() do # 1 + # # world # 2 + # if true do # 3 + # :world # 4 + # end # 5 + # end # 6 + # end # 7 + @tag cells: [{0, 0}, {1, 2}, {2, 4}, {3, 4}, {4, 6}, {5, 4}, {6, 2}, {7, 0}] + test "indent w/ successive matching levels", %{pairs: pairs} do + assert pairs == [{{0, 0}, {7, 0}}, {{1, 2}, {6, 2}}, {{3, 4}, {5, 4}}, {{2, 4}, {3, 4}}] + end + + # defmodule A do # 0 + # def f(%{"key" => value} = map) do # 1 + # case NaiveDateTime.from_iso8601(value) do # 2 + # {:ok, ndt} -> # 3 + # dt = # 4 + # ndt # 5 + # |> DateTime.from_naive!("Etc/UTC") # 6 + # |> Map.put(:microsecond, {0, 6}) # 7 + # # 8 + # %{map | "key" => dt} # 9 + # # 10 + # e -> # 11 + # Logger.warn(""" # 12 + # Could not use data map from #{inspect(value)} # 13 + # #{inspect(e)} # 14 + # """) # 15 + # # 16 + # :could_not_parse_value # 17 + # end # 18 + # end # 19 + # end # 20 + @tag cells: [ + {0, 0}, + {1, 2}, + {2, 4}, + {3, 6}, + {4, 8}, + {5, 10}, + {6, 10}, + {7, 10}, + {8, nil}, + {9, 8}, + {10, nil}, + {11, 6}, + {12, 8}, + {13, 8}, + {14, 8}, + {15, 8}, + {16, nil}, + {17, 8}, + {18, 4}, + {19, 2}, + {20, 0} + ] + test "indent w/ complicated function", %{pairs: pairs} do + pairs |> IO.inspect(label: :pairs) + + """ + defmodule A do # 0 + def f(%{"key" => value} = map) do # 1 + case NaiveDateTime.from_iso8601(value) do # 2 + {:ok, ndt} -> # 3 + dt = # 4 + ndt # 5 + |> DateTime.from_naive!("Etc/UTC") # 6 + |> Map.put(:microsecond, {0, 6}) # 7 + + %{map | "key" => dt} # 9 + + e -> # 11 + Logger.warn(\"\"\" + Could not use data map from #\{inspect(value)\} # 13 + #\{inspect(e)\} # 14 + \"\"\") + + :could_not_parse_value # 17 + end # 18 + end # 19 + end # 20 + """ + |> IO.puts() + end + + defp fold_cells(%{cells: cells} = context) do + pairs = FoldingRange.Indentation.pair_cells_2(cells) + {:ok, Map.put(context, :pairs, pairs)} + end + end + describe "genuine source files" do setup [:fold_text] From 004fd6d7bb126f7d0c37187c2dd142815ec7d10b Mon Sep 17 00:00:00 2001 From: William Lanchantin Date: Mon, 15 Feb 2021 14:19:31 -0500 Subject: [PATCH 06/61] the code caught a problem with a test! --- .../providers/folding_range/indentation.ex | 26 ++++---- .../test/providers/folding_range_test.exs | 61 +++++++++++-------- 2 files changed, 47 insertions(+), 40 deletions(-) diff --git a/apps/language_server/lib/language_server/providers/folding_range/indentation.ex b/apps/language_server/lib/language_server/providers/folding_range/indentation.ex index ba1fd9152..8b09bc9e7 100644 --- a/apps/language_server/lib/language_server/providers/folding_range/indentation.ex +++ b/apps/language_server/lib/language_server/providers/folding_range/indentation.ex @@ -91,7 +91,7 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRange.Indentation do [{first_empty_row, _} | _] = empties |> Enum.reverse() {cell1, {first_empty_row - 1, nil}} end) - |> Enum.reject(fn {{r1, _}, {r2, _}} -> r2 <= r1 + 1 end) + |> Enum.reject(fn {{r1, _}, {r2, _}} -> r1 + 1 >= r2 end) |> Enum.sort() end @@ -112,37 +112,37 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRange.Indentation do empties, pairs ) do - cur |> IO.inspect(label: :cur) - stack |> IO.inspect(label: :stack) - empties |> IO.inspect(label: :empties) - pairs |> IO.inspect(label: :pairs) + # cur |> IO.inspect(label: :cur) + # stack |> IO.inspect(label: :stack) + # empties |> IO.inspect(label: :empties) + # pairs |> IO.inspect(label: :pairs) {new_stack, new_empties, new_pairs} = cond do is_nil(col_cur) -> - "is_nil" |> IO.inspect(label: :clause) + # "is_nil" |> IO.inspect(label: :clause) {stack, [cur | empties], pairs} col_cur > col_top -> - ">" |> IO.inspect(label: :clause) + # ">" |> IO.inspect(label: :clause) {[cur | stack], [], pairs} col_cur == col_top -> - "==" |> IO.inspect(label: :clause) + # "==" |> IO.inspect(label: :clause) {[cur | tail_stack], [], [{top, cur, empties} | pairs]} col_cur < col_top -> - "<" |> IO.inspect(label: :clause) + # "<" |> IO.inspect(label: :clause) gr? = fn {_, c} -> col_cur <= c end {leftovers, new_tail_stack} = stack |> Enum.split_while(gr?) - stack |> Enum.split_while(gr?) |> IO.inspect(label: :lists) - leftovers |> IO.inspect() - new_tail_stack |> IO.inspect(label: :new_tail_stack) + # stack |> Enum.split_while(gr?) |> IO.inspect(label: :lists) + # leftovers |> IO.inspect() + # new_tail_stack |> IO.inspect(label: :new_tail_stack) new_pairs = leftovers |> Enum.map(&{&1, cur, empties}) {new_tail_stack, [], new_pairs ++ pairs} end - "" |> IO.puts() + # "" |> IO.puts() do_pair_cells_2(tail_cells, new_stack, new_empties, new_pairs) end diff --git a/apps/language_server/test/providers/folding_range_test.exs b/apps/language_server/test/providers/folding_range_test.exs index 11a6de75a..d904173a7 100644 --- a/apps/language_server/test/providers/folding_range_test.exs +++ b/apps/language_server/test/providers/folding_range_test.exs @@ -30,7 +30,7 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRangeTest do # end # 7 @tag cells: [{0, 0}, {1, 2}, {2, 4}, {3, 4}, {4, 6}, {5, 4}, {6, 2}, {7, 0}] test "indent w/ successive matching levels", %{pairs: pairs} do - assert pairs == [{{0, 0}, {7, 0}}, {{1, 2}, {6, 2}}, {{3, 4}, {5, 4}}, {{2, 4}, {3, 4}}] + assert pairs == [{{0, 0}, {7, 0}}, {{1, 2}, {6, 2}}, {{3, 4}, {5, 4}}] end # defmodule A do # 0 @@ -78,32 +78,39 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRangeTest do {20, 0} ] test "indent w/ complicated function", %{pairs: pairs} do - pairs |> IO.inspect(label: :pairs) - - """ - defmodule A do # 0 - def f(%{"key" => value} = map) do # 1 - case NaiveDateTime.from_iso8601(value) do # 2 - {:ok, ndt} -> # 3 - dt = # 4 - ndt # 5 - |> DateTime.from_naive!("Etc/UTC") # 6 - |> Map.put(:microsecond, {0, 6}) # 7 - - %{map | "key" => dt} # 9 - - e -> # 11 - Logger.warn(\"\"\" - Could not use data map from #\{inspect(value)\} # 13 - #\{inspect(e)\} # 14 - \"\"\") - - :could_not_parse_value # 17 - end # 18 - end # 19 - end # 20 - """ - |> IO.puts() + assert pairs == [ + {{0, 0}, {20, 0}}, + {{1, 2}, {19, 2}}, + {{2, 4}, {18, 4}}, + {{3, 6}, {9, nil}}, + {{4, 8}, {7, nil}}, + {{11, 6}, {18, 4}} + ] + + # """ + # defmodule A do # 0 + # def f(%{"key" => value} = map) do # 1 + # case NaiveDateTime.from_iso8601(value) do # 2 + # {:ok, ndt} -> # 3 + # dt = # 4 + # ndt # 5 + # |> DateTime.from_naive!("Etc/UTC") # 6 + # |> Map.put(:microsecond, {0, 6}) # 7 + + # %{map | "key" => dt} # 9 + + # e -> # 11 + # Logger.warn(\"\"\" + # Could not use data map from #\{inspect(value)\} # 13 + # #\{inspect(e)\} # 14 + # \"\"\") + + # :could_not_parse_value # 17 + # end # 18 + # end # 19 + # end # 20 + # """ + # |> IO.puts() end defp fold_cells(%{cells: cells} = context) do From 8269e8684f125791f4e098f24ecb9e61279fdef5 Mon Sep 17 00:00:00 2001 From: William Lanchantin Date: Mon, 15 Feb 2021 15:43:46 -0500 Subject: [PATCH 07/61] clean up a little --- .../providers/folding_range/indentation.ex | 138 +++--------------- .../test/providers/folding_range_test.exs | 31 +--- 2 files changed, 20 insertions(+), 149 deletions(-) diff --git a/apps/language_server/lib/language_server/providers/folding_range/indentation.ex b/apps/language_server/lib/language_server/providers/folding_range/indentation.ex index 8b09bc9e7..d45f98819 100644 --- a/apps/language_server/lib/language_server/providers/folding_range/indentation.ex +++ b/apps/language_server/lib/language_server/providers/folding_range/indentation.ex @@ -6,10 +6,8 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRange.Indentation do @doc """ """ def provide_ranges(text) do - cells = find_cells(text) |> IO.inspect(label: :cells) - ranges = pair_cells(cells, [], []) - ranges_2 = pair_cells_2(cells) - ranges_2 |> IO.inspect(label: :ranges_2) + cells = find_cells(text) + ranges = pair_cells(cells) {:ok, ranges} end @@ -30,58 +28,12 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRange.Indentation do end) end - defp pair_cells([], _, pairs), do: pairs - - defp pair_cells([cell | rest], [], pairs) do - pair_cells(rest, [cell], pairs) + def pair_cells(cells) do + do_pair_cells(cells, [], [], []) end - defp pair_cells( - [{row_cur, col_cur} = cur | rest], - [{_, col_top} = top | tail_stack] = stack, - pairs - ) do - # cur |> IO.inspect(label: :current) - # top |> IO.inspect(label: :top_stack) - # tail_stack |> IO.inspect(label: :tail_stack) - # pairs |> IO.inspect(label: :pairs) - # "" |> IO.puts() - - {new_stack, new_pairs} = - cond do - is_nil(col_cur) -> - case stack do - [right | [left | new_tail_stack]] -> - {new_tail_stack, [{left, right} | pairs]} - - _ -> - {tail_stack, pairs} - end - - col_cur > col_top -> - {[cur | stack], pairs} - - col_cur == col_top -> - {tail_stack, [{top, {row_cur - 1, col_cur}} | pairs]} - - col_cur < col_top -> - case tail_stack do - [match | new_tail_stack] -> - {new_tail_stack, [{match, {row_cur - 1, col_cur}} | pairs]} - - _ -> - {tail_stack, pairs} - end - end - - pair_cells(rest, new_stack, new_pairs) - end - - def pair_cells_2(cells) do - do_pair_cells_2(cells, [], [], []) - end - - defp do_pair_cells_2([], _, _, pairs) do + # Base case + defp do_pair_cells([], _, _, pairs) do pairs |> Enum.map(fn {cell1, cell2, []} -> @@ -95,96 +47,40 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRange.Indentation do |> Enum.sort() end - defp do_pair_cells_2([{_, col} = cell | tail], [], empties, pairs) do - {new_stack, new_empties} = - if is_nil(col) do - {[], [cell | empties]} - else - {[cell], empties} - end + # Empty stack with empty row + defp do_pair_cells([{_, nil} = cell | tail], [], empties, pairs) do + do_pair_cells(tail, [], [cell | empties], pairs) + end - do_pair_cells_2(tail, new_stack, new_empties, pairs) + # Empty stack + defp do_pair_cells([cell | tail], [], empties, pairs) do + do_pair_cells(tail, [cell], empties, pairs) end - defp do_pair_cells_2( + # Non-empty stack + defp do_pair_cells( [{_, col_cur} = cur | tail_cells], [{_, col_top} = top | tail_stack] = stack, empties, pairs ) do - # cur |> IO.inspect(label: :cur) - # stack |> IO.inspect(label: :stack) - # empties |> IO.inspect(label: :empties) - # pairs |> IO.inspect(label: :pairs) - {new_stack, new_empties, new_pairs} = cond do is_nil(col_cur) -> - # "is_nil" |> IO.inspect(label: :clause) {stack, [cur | empties], pairs} col_cur > col_top -> - # ">" |> IO.inspect(label: :clause) {[cur | stack], [], pairs} col_cur == col_top -> - # "==" |> IO.inspect(label: :clause) {[cur | tail_stack], [], [{top, cur, empties} | pairs]} col_cur < col_top -> - # "<" |> IO.inspect(label: :clause) - gr? = fn {_, c} -> col_cur <= c end - {leftovers, new_tail_stack} = stack |> Enum.split_while(gr?) - # stack |> Enum.split_while(gr?) |> IO.inspect(label: :lists) - # leftovers |> IO.inspect() - # new_tail_stack |> IO.inspect(label: :new_tail_stack) + {leftovers, new_tail_stack} = stack |> Enum.split_while(fn {_, c} -> col_cur <= c end) new_pairs = leftovers |> Enum.map(&{&1, cur, empties}) {new_tail_stack, [], new_pairs ++ pairs} end - # "" |> IO.puts() - do_pair_cells_2(tail_cells, new_stack, new_empties, new_pairs) + do_pair_cells(tail_cells, new_stack, new_empties, new_pairs) end - - # defp pair_cells_nsq([], pairs) do - # pairs - # |> IO.inspect() - # |> Enum.reduce([], fn {{_r1, c1}, {_r2, c2}} = pair, pairs -> - # if c1 > c2 do - # pairs - # else - # [pair | pairs] - # end - # end) - # end - - # defp pair_cells_nsq([{_, nil} | tail], pairs) do - # pair_cells_nsq(tail, pairs) - # end - - # defp pair_cells_nsq([{_, ch} = head | tail], pairs) do - # first_leq = Enum.find(tail, fn {_, ct} -> ct <= ch end) - # # head |> IO.inspect(label: :head) - # # first_leq |> IO.inspect(label: :first_leq) - # # pairs |> IO.inspect(label: :pairs) - # # "" |> IO.puts() - - # new_pairs = - # case first_leq do - # nil -> pairs - # first_leq -> [{head, first_leq} | pairs] - # end - - # pair_cells_nsq(tail, new_pairs) - # end - - # defp collapse_nil(pairs, cells) do - # search = cells |> Enum.reverse() - - # pairs - # |> Enum.map(fn {cell1, {r2, _}} -> - # cell2 = Enum.find(search, fn {r, c} -> r <= r2 and !is_nil(c) end) - # {cell1, cell2} - # end) - # end end diff --git a/apps/language_server/test/providers/folding_range_test.exs b/apps/language_server/test/providers/folding_range_test.exs index d904173a7..124e83e7a 100644 --- a/apps/language_server/test/providers/folding_range_test.exs +++ b/apps/language_server/test/providers/folding_range_test.exs @@ -8,7 +8,7 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRangeTest do end describe "indentation" do - setup [:fold_cells] + setup [:pair_cells] # defmodule A do # 0 # def hello() do # 1 @@ -86,35 +86,10 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRangeTest do {{4, 8}, {7, nil}}, {{11, 6}, {18, 4}} ] - - # """ - # defmodule A do # 0 - # def f(%{"key" => value} = map) do # 1 - # case NaiveDateTime.from_iso8601(value) do # 2 - # {:ok, ndt} -> # 3 - # dt = # 4 - # ndt # 5 - # |> DateTime.from_naive!("Etc/UTC") # 6 - # |> Map.put(:microsecond, {0, 6}) # 7 - - # %{map | "key" => dt} # 9 - - # e -> # 11 - # Logger.warn(\"\"\" - # Could not use data map from #\{inspect(value)\} # 13 - # #\{inspect(e)\} # 14 - # \"\"\") - - # :could_not_parse_value # 17 - # end # 18 - # end # 19 - # end # 20 - # """ - # |> IO.puts() end - defp fold_cells(%{cells: cells} = context) do - pairs = FoldingRange.Indentation.pair_cells_2(cells) + defp pair_cells(%{cells: cells} = context) do + pairs = FoldingRange.Indentation.pair_cells(cells) {:ok, Map.put(context, :pairs, pairs)} end end From 1b9c0c4ef72c598ef0e095e2f3b4213e9c879ad0 Mon Sep 17 00:00:00 2001 From: William Lanchantin Date: Mon, 15 Feb 2021 15:53:45 -0500 Subject: [PATCH 08/61] add some explanatory comments --- .../language_server/providers/folding_range/indentation.ex | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/apps/language_server/lib/language_server/providers/folding_range/indentation.ex b/apps/language_server/lib/language_server/providers/folding_range/indentation.ex index d45f98819..178225a5d 100644 --- a/apps/language_server/lib/language_server/providers/folding_range/indentation.ex +++ b/apps/language_server/lib/language_server/providers/folding_range/indentation.ex @@ -73,9 +73,15 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRange.Indentation do {[cur | stack], [], pairs} col_cur == col_top -> + # An exact match can be the end of one pair and the start of another. + # E.g.: The else in an if-do-else-end block {[cur | tail_stack], [], [{top, cur, empties} | pairs]} col_cur < col_top -> + # If the current column is further to the left than that of the top + # of the stack, then we need to pair it with everything on the stack + # to the right of it. + # E.g.: The end with the clause of a case-do-end block {leftovers, new_tail_stack} = stack |> Enum.split_while(fn {_, c} -> col_cur <= c end) new_pairs = leftovers |> Enum.map(&{&1, cur, empties}) {new_tail_stack, [], new_pairs ++ pairs} From 84604ce3a003843dd101b505618bde32dc8c2c83 Mon Sep 17 00:00:00 2001 From: William Lanchantin Date: Mon, 15 Feb 2021 16:02:28 -0500 Subject: [PATCH 09/61] round out functionality --- .../providers/folding_range/indentation.ex | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/apps/language_server/lib/language_server/providers/folding_range/indentation.ex b/apps/language_server/lib/language_server/providers/folding_range/indentation.ex index 178225a5d..c7ccefee1 100644 --- a/apps/language_server/lib/language_server/providers/folding_range/indentation.ex +++ b/apps/language_server/lib/language_server/providers/folding_range/indentation.ex @@ -4,10 +4,16 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRange.Indentation do """ @doc """ + Provides ranges for the source text based on the indentation level. + Note that we trim trailing empy rows from regions. """ def provide_ranges(text) do - cells = find_cells(text) - ranges = pair_cells(cells) + ranges = + text + |> find_cells() + |> pair_cells() + |> pairs_to_ranges() + {:ok, ranges} end @@ -89,4 +95,11 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRange.Indentation do do_pair_cells(tail_cells, new_stack, new_empties, new_pairs) end + + defp pairs_to_ranges(pairs) do + pairs + |> Enum.map(fn + {{r1, _}, {r2, _}} -> %{"startLine" => r1, "endLine" => r2, "kind?" => "region"} + end) + end end From 828963449f6a18b37b02c8026ec65edf933ca8b0 Mon Sep 17 00:00:00 2001 From: William Lanchantin Date: Mon, 15 Feb 2021 16:42:09 -0500 Subject: [PATCH 10/61] fix some off-by-1 nonsense --- .../language_server/providers/folding_range/indentation.ex | 6 +++--- apps/language_server/test/providers/folding_range_test.exs | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/language_server/lib/language_server/providers/folding_range/indentation.ex b/apps/language_server/lib/language_server/providers/folding_range/indentation.ex index c7ccefee1..c71ad1679 100644 --- a/apps/language_server/lib/language_server/providers/folding_range/indentation.ex +++ b/apps/language_server/lib/language_server/providers/folding_range/indentation.ex @@ -46,8 +46,8 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRange.Indentation do {cell1, cell2} {cell1, _, empties} -> - [{first_empty_row, _} | _] = empties |> Enum.reverse() - {cell1, {first_empty_row - 1, nil}} + [first_empty_cell | _] = empties |> Enum.reverse() + {cell1, first_empty_cell} end) |> Enum.reject(fn {{r1, _}, {r2, _}} -> r1 + 1 >= r2 end) |> Enum.sort() @@ -99,7 +99,7 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRange.Indentation do defp pairs_to_ranges(pairs) do pairs |> Enum.map(fn - {{r1, _}, {r2, _}} -> %{"startLine" => r1, "endLine" => r2, "kind?" => "region"} + {{r1, _}, {r2, _}} -> %{"startLine" => r1, "endLine" => r2 - 1, "kind?" => "region"} end) end end diff --git a/apps/language_server/test/providers/folding_range_test.exs b/apps/language_server/test/providers/folding_range_test.exs index 124e83e7a..b5c21384c 100644 --- a/apps/language_server/test/providers/folding_range_test.exs +++ b/apps/language_server/test/providers/folding_range_test.exs @@ -82,8 +82,8 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRangeTest do {{0, 0}, {20, 0}}, {{1, 2}, {19, 2}}, {{2, 4}, {18, 4}}, - {{3, 6}, {9, nil}}, - {{4, 8}, {7, nil}}, + {{3, 6}, {10, nil}}, + {{4, 8}, {8, nil}}, {{11, 6}, {18, 4}} ] end From c7c56f6ba83a573e318920c7631ba201f6a8b208 Mon Sep 17 00:00:00 2001 From: William Lanchantin Date: Mon, 15 Feb 2021 16:43:40 -0500 Subject: [PATCH 11/61] grammar --- .../lib/language_server/providers/folding_range/indentation.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/language_server/lib/language_server/providers/folding_range/indentation.ex b/apps/language_server/lib/language_server/providers/folding_range/indentation.ex index c71ad1679..e99140f5e 100644 --- a/apps/language_server/lib/language_server/providers/folding_range/indentation.ex +++ b/apps/language_server/lib/language_server/providers/folding_range/indentation.ex @@ -84,7 +84,7 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRange.Indentation do {[cur | tail_stack], [], [{top, cur, empties} | pairs]} col_cur < col_top -> - # If the current column is further to the left than that of the top + # If the current column is farther to the left than that of the top # of the stack, then we need to pair it with everything on the stack # to the right of it. # E.g.: The end with the clause of a case-do-end block From bc8587f7ba31bec3fc221e0831ab89cd5dfad340 Mon Sep 17 00:00:00 2001 From: William Lanchantin Date: Mon, 15 Feb 2021 16:45:17 -0500 Subject: [PATCH 12/61] remember to add cur to the stack in all cases! --- .../language_server/providers/folding_range/indentation.ex | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/language_server/lib/language_server/providers/folding_range/indentation.ex b/apps/language_server/lib/language_server/providers/folding_range/indentation.ex index e99140f5e..aec205ad5 100644 --- a/apps/language_server/lib/language_server/providers/folding_range/indentation.ex +++ b/apps/language_server/lib/language_server/providers/folding_range/indentation.ex @@ -88,9 +88,11 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRange.Indentation do # of the stack, then we need to pair it with everything on the stack # to the right of it. # E.g.: The end with the clause of a case-do-end block + # And as with the == clause, the current cell could also be the start + # of a new block. {leftovers, new_tail_stack} = stack |> Enum.split_while(fn {_, c} -> col_cur <= c end) new_pairs = leftovers |> Enum.map(&{&1, cur, empties}) - {new_tail_stack, [], new_pairs ++ pairs} + {[cur | new_tail_stack], [], new_pairs ++ pairs} end do_pair_cells(tail_cells, new_stack, new_empties, new_pairs) From 1567d42da73da2003c0a3be337b4a54a59f26da4 Mon Sep 17 00:00:00 2001 From: William Lanchantin Date: Mon, 15 Feb 2021 17:59:11 -0500 Subject: [PATCH 13/61] adjust test --- .../test/providers/folding_range_test.exs | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/apps/language_server/test/providers/folding_range_test.exs b/apps/language_server/test/providers/folding_range_test.exs index b5c21384c..2aa44498a 100644 --- a/apps/language_server/test/providers/folding_range_test.exs +++ b/apps/language_server/test/providers/folding_range_test.exs @@ -88,6 +88,45 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRangeTest do ] end + # defmodule A do # 0 + # def get_info(args) do # 1 + # org = # 2 + # args # 3 + # |> Ecto.assoc(:organization) # 4 + # |> Repo.one!() # 5 + # # 6 + # user = # 7 + # org # 8 + # |> Organization.user!() # 9 + # # 10 + # {:ok, %{org: org, user: user}} # 11 + # end # 12 + # end # 13 + @tag cells: [ + {0, 0}, + {1, 2}, + {2, 4}, + {3, 6}, + {4, 6}, + {5, 6}, + {6, nil}, + {7, 4}, + {8, 6}, + {9, 6}, + {10, nil}, + {11, 4}, + {12, 2}, + {13, 0} + ] + test "indent w/ different complicated function", %{pairs: pairs} do + assert pairs == [ + {{0, 0}, {13, 0}}, + {{1, 2}, {12, 2}}, + {{2, 4}, {6, nil}}, + {{7, 4}, {10, nil}} + ] + end + defp pair_cells(%{cells: cells} = context) do pairs = FoldingRange.Indentation.pair_cells(cells) {:ok, Map.put(context, :pairs, pairs)} From 270b57d7050f86005f124cd47497cb14825d74c7 Mon Sep 17 00:00:00 2001 From: William Lanchantin Date: Mon, 15 Feb 2021 18:22:14 -0500 Subject: [PATCH 14/61] whoo! case reduction! --- .../providers/folding_range/indentation.ex | 54 ++++++------------- 1 file changed, 17 insertions(+), 37 deletions(-) diff --git a/apps/language_server/lib/language_server/providers/folding_range/indentation.ex b/apps/language_server/lib/language_server/providers/folding_range/indentation.ex index aec205ad5..ff2e505a4 100644 --- a/apps/language_server/lib/language_server/providers/folding_range/indentation.ex +++ b/apps/language_server/lib/language_server/providers/folding_range/indentation.ex @@ -53,49 +53,29 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRange.Indentation do |> Enum.sort() end - # Empty stack with empty row - defp do_pair_cells([{_, nil} = cell | tail], [], empties, pairs) do - do_pair_cells(tail, [], [cell | empties], pairs) + # Empty row + defp do_pair_cells([{_, nil} = head | tail], stack, empties, pairs) do + do_pair_cells(tail, stack, [head | empties], pairs) end # Empty stack - defp do_pair_cells([cell | tail], [], empties, pairs) do - do_pair_cells(tail, [cell], empties, pairs) + defp do_pair_cells([head | tail], [], empties, pairs) do + do_pair_cells(tail, [head], empties, pairs) end - # Non-empty stack - defp do_pair_cells( - [{_, col_cur} = cur | tail_cells], - [{_, col_top} = top | tail_stack] = stack, - empties, - pairs - ) do - {new_stack, new_empties, new_pairs} = - cond do - is_nil(col_cur) -> - {stack, [cur | empties], pairs} - - col_cur > col_top -> - {[cur | stack], [], pairs} - - col_cur == col_top -> - # An exact match can be the end of one pair and the start of another. - # E.g.: The else in an if-do-else-end block - {[cur | tail_stack], [], [{top, cur, empties} | pairs]} - - col_cur < col_top -> - # If the current column is farther to the left than that of the top - # of the stack, then we need to pair it with everything on the stack - # to the right of it. - # E.g.: The end with the clause of a case-do-end block - # And as with the == clause, the current cell could also be the start - # of a new block. - {leftovers, new_tail_stack} = stack |> Enum.split_while(fn {_, c} -> col_cur <= c end) - new_pairs = leftovers |> Enum.map(&{&1, cur, empties}) - {[cur | new_tail_stack], [], new_pairs ++ pairs} - end + # Non-empty stack: head is to the right of the top of the stack + defp do_pair_cells([{_, x} = head | tail], [{_, y} | _] = stack, _, pairs) when x > y do + do_pair_cells(tail, [head | stack], [], pairs) + end - do_pair_cells(tail_cells, new_stack, new_empties, new_pairs) + # Non-empty stack: head is equal to or to the left of the top of the stack + defp do_pair_cells([{_, x} = head | tail], stack, empties, pairs) do + # If the head is <= to the top of the stack, then we need to pair it with + # everything on the stack to the right of it. + # The head can also be the start of a new region, so it's pushed onto the stack. + {leftovers, new_tail_stack} = stack |> Enum.split_while(fn {_, y} -> x <= y end) + new_pairs = leftovers |> Enum.map(&{&1, head, empties}) + do_pair_cells(tail, [head | new_tail_stack], [], new_pairs ++ pairs) end defp pairs_to_ranges(pairs) do From 249b673030c8bd8b33cb327c51dfb806cdfccb58 Mon Sep 17 00:00:00 2001 From: William Lanchantin Date: Mon, 15 Feb 2021 18:25:23 -0500 Subject: [PATCH 15/61] shorten comment --- .../lib/language_server/providers/folding_range/indentation.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/language_server/lib/language_server/providers/folding_range/indentation.ex b/apps/language_server/lib/language_server/providers/folding_range/indentation.ex index ff2e505a4..d89713d9e 100644 --- a/apps/language_server/lib/language_server/providers/folding_range/indentation.ex +++ b/apps/language_server/lib/language_server/providers/folding_range/indentation.ex @@ -72,7 +72,7 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRange.Indentation do defp do_pair_cells([{_, x} = head | tail], stack, empties, pairs) do # If the head is <= to the top of the stack, then we need to pair it with # everything on the stack to the right of it. - # The head can also be the start of a new region, so it's pushed onto the stack. + # The head can also start a new region, so it's pushed onto the stack. {leftovers, new_tail_stack} = stack |> Enum.split_while(fn {_, y} -> x <= y end) new_pairs = leftovers |> Enum.map(&{&1, head, empties}) do_pair_cells(tail, [head | new_tail_stack], [], new_pairs ++ pairs) From 8ff26cf5e70035e2abafc11e3497ddf1ac0bfa19 Mon Sep 17 00:00:00 2001 From: William Lanchantin Date: Mon, 15 Feb 2021 18:38:18 -0500 Subject: [PATCH 16/61] tweaks --- .../providers/folding_range/indentation.ex | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/language_server/lib/language_server/providers/folding_range/indentation.ex b/apps/language_server/lib/language_server/providers/folding_range/indentation.ex index d89713d9e..6ac4c965d 100644 --- a/apps/language_server/lib/language_server/providers/folding_range/indentation.ex +++ b/apps/language_server/lib/language_server/providers/folding_range/indentation.ex @@ -34,6 +34,10 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRange.Indentation do end) end + @doc """ + Pairs cells into {start, end} tuples of regions + Public function for testing + """ def pair_cells(cells) do do_pair_cells(cells, [], [], []) end @@ -42,12 +46,8 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRange.Indentation do defp do_pair_cells([], _, _, pairs) do pairs |> Enum.map(fn - {cell1, cell2, []} -> - {cell1, cell2} - - {cell1, _, empties} -> - [first_empty_cell | _] = empties |> Enum.reverse() - {cell1, first_empty_cell} + {cell1, cell2, []} -> {cell1, cell2} + {cell1, _, empties} -> {cell1, List.last(empties)} end) |> Enum.reject(fn {{r1, _}, {r2, _}} -> r1 + 1 >= r2 end) |> Enum.sort() From f79664cf677cc333e540920851082fa8cedbe6b8 Mon Sep 17 00:00:00 2001 From: William Lanchantin Date: Tue, 16 Feb 2021 09:07:24 -0500 Subject: [PATCH 17/61] better formatting --- .../language_server/providers/folding_range/indentation.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/language_server/lib/language_server/providers/folding_range/indentation.ex b/apps/language_server/lib/language_server/providers/folding_range/indentation.ex index 6ac4c965d..28f7a89d7 100644 --- a/apps/language_server/lib/language_server/providers/folding_range/indentation.ex +++ b/apps/language_server/lib/language_server/providers/folding_range/indentation.ex @@ -80,8 +80,8 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRange.Indentation do defp pairs_to_ranges(pairs) do pairs - |> Enum.map(fn - {{r1, _}, {r2, _}} -> %{"startLine" => r1, "endLine" => r2 - 1, "kind?" => "region"} + |> Enum.map(fn {{r1, _}, {r2, _}} -> + %{"startLine" => r1, "endLine" => r2 - 1, "kind?" => "region"} end) end end From 0f52639999320b099af737e49df4af670ed78d80 Mon Sep 17 00:00:00 2001 From: William Lanchantin Date: Tue, 16 Feb 2021 10:27:03 -0500 Subject: [PATCH 18/61] add token-pairs module; start adding typespecs --- .../providers/folding_range.ex | 243 ++---------------- .../providers/folding_range/indentation.ex | 15 +- .../providers/folding_range/token_pairs.ex | 94 +++++++ 3 files changed, 130 insertions(+), 222 deletions(-) create mode 100644 apps/language_server/lib/language_server/providers/folding_range/token_pairs.ex diff --git a/apps/language_server/lib/language_server/providers/folding_range.ex b/apps/language_server/lib/language_server/providers/folding_range.ex index c1d1a56c0..e3b2449d5 100644 --- a/apps/language_server/lib/language_server/providers/folding_range.ex +++ b/apps/language_server/lib/language_server/providers/folding_range.ex @@ -7,21 +7,19 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRange do ## TODO - - [ ] Indentation pass + - [x] Indentation pass - [ ] Add priorities and do a proper merge """ alias ElixirSense.Core.Normalized.Tokenizer - @basic_pairs %{ - do: :end, - "(": :")", - "[": :"]", - "{": :"}" - } - @heredoc_pairs %{ - bin_heredoc: :eol - } + @type t :: %{ + required(:startLine) => non_neg_integer(), + required(:endLine) => non_neg_integer(), + optional(:startCharacter?) => non_neg_integer(), + optional(:endCharacter?) => non_neg_integer(), + optional(:kind?) => :comment | :imports | :region + } @doc """ Provides folding ranges for a source file @@ -44,8 +42,7 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRange do # %{"startLine" => 1, "endLine" => 2} # ] """ - @spec provide(%{text: String.t()}) :: - {:ok, [%{required(String.t()) => non_neg_integer()}]} | {:error, String.t()} + @spec provide(%{text: String.t()}) :: {:ok, [t()]} | {:error, String.t()} def provide(%{text: text}) do do_provide(text) end @@ -55,21 +52,15 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRange do end defp do_provide(text) do - _ranges = - text - |> Tokenizer.tokenize() - |> format_tokens() - |> case do - {:ok, formatted_tokens} -> formatted_tokens |> fold_tokens_into_ranges() - _ -> [] - end - - ranges = text |> from_indentation() - + formatted_tokens = text |> Tokenizer.tokenize() |> format_tokens() + {:ok, token_pair_ranges} = formatted_tokens |> __MODULE__.TokenPairs.provide_ranges() + {:ok, indentation_ranges} = text |> __MODULE__.Indentation.provide_ranges() + ranges = merge_ranges(token_pair_ranges ++ indentation_ranges) {:ok, ranges} end - # Make pattern-matching easier by forcing all tuples to be 3-tuples + # Make pattern-matching easier by forcing all tuples to be 3-tuples. + # Also convert to 0-indexing as ranges are 0-indexed. defp format_tokens(reversed_tokens) when is_list(reversed_tokens) do reversed_tokens # This reverses the tokens, but they come out of Tokenizer.tokenize/1 @@ -77,8 +68,8 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRange do |> Enum.reduce_while({:ok, []}, fn tuple, {:ok, acc} -> tuple = case tuple do - {a, b} -> {a, b, nil} - {a, b, c} -> {a, b, c} + {a, {b1, b2, b3}} -> {a, {b1 - 1, b2 - 1, b3}, nil} + {a, {b1, b2, b3}, c} -> {a, {b1 - 1, b2 - 1, b3}, c} # raise here? _ -> :error end @@ -89,201 +80,13 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRange do {:cont, {:ok, [tuple | acc]}} end end) + |> case do + {:ok, formatted_tokens} -> formatted_tokens + _ -> [] + end end - # Note - # This implementation allows for the possibility of 2 ranges with the same - # startLines but different endLines. - # It's not clear if that case is actually a problem. - defp fold_tokens_into_ranges(tokens) when is_list(tokens) do - ranges_from_pairs = tokens |> pair_tokens(@basic_pairs) - ranges_from_heredocs = tokens |> pair_tokens(@heredoc_pairs) - ranges = ranges_from_pairs ++ ranges_from_heredocs - ranges |> convert_to_spec_ranges() - end - - defp convert_to_spec_ranges(ranges) do - ranges - |> Enum.filter(fn {start_line, end_line} -> end_line > start_line end) - |> Enum.sort() - |> Enum.dedup() - ## Remove the above sort + dedup lines and uncomment the following if no - ## two ranges may share a startLine - # |> Enum.group_by(fn {start_line, _} -> start_line end) - # |> Enum.map(fn {_, ranges} -> - # Enum.max_by(ranges, fn {_, end_line} -> end_line end) - # end) - |> Enum.map(fn {start_line, end_line} -> - %{"startLine" => start_line, "endLine" => end_line} - end) - end - - defp pair_tokens(tokens, kind_map) do - tokens - |> do_pair_tokens([], [], kind_map) - |> Enum.map(fn {{_, {start_line, _, _}, _}, {_, {end_line, _, _}, _}} -> - # -1 for both because the server expects 0-indexing - # Another -1 for end_line because the range should stop 1 short - # e.g. both "do" and "end" should be visible when collapsed - {start_line - 1, end_line - 2} - end) - end - - # A stack-based approach to match range pairs - # Notes - # - The returned pairs will be ordered by the line of the 2nd element. - # - Tokenizer.tokenize/1 doesn't differentiate between successful and failed - # attempts to tokenize the string. - # This could mean the returned tokens are unbalaned. - # Therefore, the stack may not be empty when the base clause is hit. - # We're choosing to return the successfully paired tokens rather than to - # return an error if not all tokens could be paired. - defp do_pair_tokens([], _stack, pairs, _kind_map), do: pairs - - defp do_pair_tokens([{start_kind, _, _} = start | tail_tokens], [], pairs, kind_map) do - new_stack = if Map.get(kind_map, start_kind), do: [start], else: [] - do_pair_tokens(tail_tokens, new_stack, pairs, kind_map) - end - - defp do_pair_tokens( - [{start_kind, _, _} = start | tail_tokens], - [{top_kind, _, _} = top | tail_stack] = stack, - pairs, - kind_map - ) do - {new_stack, new_pairs} = - cond do - Map.get(kind_map, top_kind) == start_kind -> - {tail_stack, [{top, start} | pairs]} - - Map.get(kind_map, start_kind) -> - {[start | stack], pairs} - - true -> - {stack, pairs} - end - - do_pair_tokens(tail_tokens, new_stack, new_pairs, kind_map) - end - - defp from_indentation(text) do - cells = - text - |> String.split("\n") - |> Enum.with_index() - |> Enum.map(fn {line, row} -> - full = line |> String.length() - trimmed = line |> String.trim_leading() |> String.length() - col = if {full, trimmed} == {0, 0}, do: nil, else: full - trimmed + 1 - {row, col} - end) - - # |> Enum.chunk_by(fn {_, col} -> col end) - # |> Enum.map(fn - # [x] -> x - # list -> list |> List.last() - # end) - # |> IO.inspect() - cells - # |> pair_cells([], []) - |> pair_cells_nsq([]) - # |> Enum.sort() - |> IO.inspect() - |> collapse_nil(cells) - |> IO.inspect() - |> Enum.reject(fn {{r1, _}, {r2, _}} -> r1 >= r2 end) - |> IO.inspect() - |> Enum.map(fn {{start_line, _}, {end_line, _}} -> - %{"startLine" => start_line, "endLine" => end_line} - end) - end - - # defp pair_cells([], _, pairs), do: pairs - - # defp pair_cells([cell | rest], [], pairs) do - # pair_cells(rest, [cell], pairs) - # end - - # defp pair_cells( - # [{row_cur, col_cur} = cur | rest], - # [{_, col_top} = top | tail_stack] = stack, - # pairs - # ) do - # # cur |> IO.inspect(label: :current) - # # top |> IO.inspect(label: :top_stack) - # # tail_stack |> IO.inspect(label: :tail_stack) - # # pairs |> IO.inspect(label: :pairs) - # # "" |> IO.puts() - - # {new_stack, new_pairs} = - # cond do - # is_nil(col_cur) -> - # case stack do - # [right | [left | new_tail_stack]] -> - # {new_tail_stack, [{left, right} | pairs]} - - # _ -> - # {tail_stack, pairs} - # end - - # col_cur > col_top -> - # {[cur | stack], pairs} - - # col_cur == col_top -> - # {tail_stack, [{top, {row_cur - 1, col_cur}} | pairs]} - - # col_cur < col_top -> - # case tail_stack do - # [match | new_tail_stack] -> - # {new_tail_stack, [{match, {row_cur - 1, col_cur}} | pairs]} - - # _ -> - # {tail_stack, pairs} - # end - # end - - # pair_cells(rest, new_stack, new_pairs) - # end - - defp pair_cells_nsq([], pairs) do - pairs - |> IO.inspect() - |> Enum.reduce([], fn {{_r1, c1}, {_r2, c2}} = pair, pairs -> - if c1 > c2 do - pairs - else - [pair | pairs] - end - end) - end - - defp pair_cells_nsq([{_, nil} | tail], pairs) do - pair_cells_nsq(tail, pairs) - end - - defp pair_cells_nsq([{_, ch} = head | tail], pairs) do - first_leq = Enum.find(tail, fn {_, ct} -> ct <= ch end) - # head |> IO.inspect(label: :head) - # first_leq |> IO.inspect(label: :first_leq) - # pairs |> IO.inspect(label: :pairs) - # "" |> IO.puts() - - new_pairs = - case first_leq do - nil -> pairs - first_leq -> [{head, first_leq} | pairs] - end - - pair_cells_nsq(tail, new_pairs) - end - - defp collapse_nil(pairs, cells) do - search = cells |> Enum.reverse() - - pairs - |> Enum.map(fn {cell1, {r2, _}} -> - cell2 = Enum.find(search, fn {r, c} -> r <= r2 and !is_nil(c) end) - {cell1, cell2} - end) + defp merge_ranges(list_of_range_lists) do + list_of_range_lists |> List.first() end end diff --git a/apps/language_server/lib/language_server/providers/folding_range/indentation.ex b/apps/language_server/lib/language_server/providers/folding_range/indentation.ex index 28f7a89d7..a8c2b5d0f 100644 --- a/apps/language_server/lib/language_server/providers/folding_range/indentation.ex +++ b/apps/language_server/lib/language_server/providers/folding_range/indentation.ex @@ -3,10 +3,15 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRange.Indentation do Code folding based on indentation only. """ + alias ElixirLS.LanguageServer.Providers.FoldingRange + + @type cell :: {non_neg_integer(), non_neg_integer() | nil} + @doc """ Provides ranges for the source text based on the indentation level. Note that we trim trailing empy rows from regions. """ + @spec provide_ranges(String.t()) :: [FoldingRange.t()] def provide_ranges(text) do ranges = text @@ -20,7 +25,7 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRange.Indentation do # If we think of the code text as a grid, this function finds the cells whose # columns are the start of each row (line). # Empty rows are represented as {row, nil}. - @spec find_cells(String.t()) :: [{non_neg_integer(), non_neg_integer()}] + @spec find_cells(String.t()) :: [cell()] defp find_cells(text) do text |> String.trim() @@ -38,6 +43,7 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRange.Indentation do Pairs cells into {start, end} tuples of regions Public function for testing """ + @spec pair_cells([cell()]) :: [{cell(), cell()}] def pair_cells(cells) do do_pair_cells(cells, [], [], []) end @@ -78,10 +84,15 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRange.Indentation do do_pair_cells(tail, [head | new_tail_stack], [], new_pairs ++ pairs) end + @spec pairs_to_ranges([{cell(), cell()}]) :: [FoldingRange.t()] defp pairs_to_ranges(pairs) do pairs |> Enum.map(fn {{r1, _}, {r2, _}} -> - %{"startLine" => r1, "endLine" => r2 - 1, "kind?" => "region"} + %{ + startLine: r1, + endLine: r2 - 1, + kind?: :region + } end) end end diff --git a/apps/language_server/lib/language_server/providers/folding_range/token_pairs.ex b/apps/language_server/lib/language_server/providers/folding_range/token_pairs.ex new file mode 100644 index 000000000..5c720e2ce --- /dev/null +++ b/apps/language_server/lib/language_server/providers/folding_range/token_pairs.ex @@ -0,0 +1,94 @@ +defmodule ElixirLS.LanguageServer.Providers.FoldingRange.TokenPairs do + @moduledoc """ + """ + + @basic_pairs %{ + do: :end, + "(": :")", + "[": :"]", + "{": :"}" + } + + @heredoc_pairs %{ + bin_heredoc: :eol + } + + def provide_ranges(tokens_3_tuple) do + ranges = fold_tokens_into_ranges(tokens_3_tuple) + {:ok, ranges} + end + + # Note + # This implementation allows for the possibility of 2 ranges with the same + # startLines but different endLines. + # It's not clear if that case is actually a problem. + defp fold_tokens_into_ranges(tokens) when is_list(tokens) do + ranges_from_pairs = tokens |> pair_tokens(@basic_pairs) + ranges_from_heredocs = tokens |> pair_tokens(@heredoc_pairs) + ranges = ranges_from_pairs ++ ranges_from_heredocs + ranges |> convert_to_spec_ranges() + end + + defp convert_to_spec_ranges(ranges) do + ranges + |> Enum.filter(fn {start_line, end_line} -> end_line > start_line end) + |> Enum.sort() + |> Enum.dedup() + ## Remove the above sort + dedup lines and uncomment the following if no + ## two ranges may share a startLine + # |> Enum.group_by(fn {start_line, _} -> start_line end) + # |> Enum.map(fn {_, ranges} -> + # Enum.max_by(ranges, fn {_, end_line} -> end_line end) + # end) + |> Enum.map(fn {start_line, end_line} -> + %{startLine: start_line, endLine: end_line} + end) + end + + defp pair_tokens(tokens, kind_map) do + tokens + |> do_pair_tokens([], [], kind_map) + |> Enum.map(fn {{_, {start_line, _, _}, _}, {_, {end_line, _, _}, _}} -> + # -1 for end_line because the range should stop 1 short + # e.g. both "do" and "end" should be visible when collapsed + {start_line, end_line - 1} + end) + end + + # A stack-based approach to match range pairs + # Notes + # - The returned pairs will be ordered by the line of the 2nd element. + # - Tokenizer.tokenize/1 doesn't differentiate between successful and failed + # attempts to tokenize the string. + # This could mean the returned tokens are unbalaned. + # Therefore, the stack may not be empty when the base clause is hit. + # We're choosing to return the successfully paired tokens rather than to + # return an error if not all tokens could be paired. + defp do_pair_tokens([], _stack, pairs, _kind_map), do: pairs + + defp do_pair_tokens([{start_kind, _, _} = start | tail_tokens], [], pairs, kind_map) do + new_stack = if Map.get(kind_map, start_kind), do: [start], else: [] + do_pair_tokens(tail_tokens, new_stack, pairs, kind_map) + end + + defp do_pair_tokens( + [{start_kind, _, _} = start | tail_tokens], + [{top_kind, _, _} = top | tail_stack] = stack, + pairs, + kind_map + ) do + {new_stack, new_pairs} = + cond do + Map.get(kind_map, top_kind) == start_kind -> + {tail_stack, [{top, start} | pairs]} + + Map.get(kind_map, start_kind) -> + {[start | stack], pairs} + + true -> + {stack, pairs} + end + + do_pair_tokens(tail_tokens, new_stack, new_pairs, kind_map) + end +end From 2c9b18dfcfeea9fe36f1704ef2a23c04ab9b4403 Mon Sep 17 00:00:00 2001 From: William Lanchantin Date: Tue, 16 Feb 2021 13:09:47 -0500 Subject: [PATCH 19/61] refactor token-pairs --- .../providers/folding_range.ex | 9 +- .../providers/folding_range/token.ex | 40 +++++++++ .../providers/folding_range/token_pairs.ex | 88 ++++++++++--------- .../test/providers/folding_range_test.exs | 41 ++++----- 4 files changed, 108 insertions(+), 70 deletions(-) create mode 100644 apps/language_server/lib/language_server/providers/folding_range/token.ex diff --git a/apps/language_server/lib/language_server/providers/folding_range.ex b/apps/language_server/lib/language_server/providers/folding_range.ex index e3b2449d5..0cd993ebc 100644 --- a/apps/language_server/lib/language_server/providers/folding_range.ex +++ b/apps/language_server/lib/language_server/providers/folding_range.ex @@ -11,8 +11,6 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRange do - [ ] Add priorities and do a proper merge """ - alias ElixirSense.Core.Normalized.Tokenizer - @type t :: %{ required(:startLine) => non_neg_integer(), required(:endLine) => non_neg_integer(), @@ -52,9 +50,10 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRange do end defp do_provide(text) do - formatted_tokens = text |> Tokenizer.tokenize() |> format_tokens() + formatted_tokens = __MODULE__.Token.format_string(text) {:ok, token_pair_ranges} = formatted_tokens |> __MODULE__.TokenPairs.provide_ranges() - {:ok, indentation_ranges} = text |> __MODULE__.Indentation.provide_ranges() + # {:ok, indentation_ranges} = text |> __MODULE__.Indentation.provide_ranges() + indentation_ranges = [] ranges = merge_ranges(token_pair_ranges ++ indentation_ranges) {:ok, ranges} end @@ -87,6 +86,6 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRange do end defp merge_ranges(list_of_range_lists) do - list_of_range_lists |> List.first() + list_of_range_lists end end diff --git a/apps/language_server/lib/language_server/providers/folding_range/token.ex b/apps/language_server/lib/language_server/providers/folding_range/token.ex new file mode 100644 index 000000000..2b3a15ff4 --- /dev/null +++ b/apps/language_server/lib/language_server/providers/folding_range/token.ex @@ -0,0 +1,40 @@ +defmodule ElixirLS.LanguageServer.Providers.FoldingRange.Token do + @moduledoc """ + """ + + alias ElixirSense.Core.Normalized.Tokenizer + + @type t :: {atom(), {non_neg_integer(), non_neg_integer(), any()}, any()} + + @doc """ + Make pattern-matching easier by forcing all token tuples to be 3-tuples. + Also convert start_info to 0-indexing as ranges are 0-indexed. + """ + @spec format_string(String.t()) :: [t()] + def format_string(text) do + reversed_tokens = text |> Tokenizer.tokenize() + + reversed_tokens + # This reverses the tokens, but they come out of Tokenizer.tokenize/1 + # already reversed. + |> Enum.reduce_while({:ok, []}, fn tuple, {:ok, acc} -> + tuple = + case tuple do + {a, {b1, b2, b3}} -> {a, {b1 - 1, b2 - 1, b3}, nil} + {a, {b1, b2, b3}, c} -> {a, {b1 - 1, b2 - 1, b3}, c} + # raise here? + _ -> :error + end + + if tuple == :error do + {:halt, :error} + else + {:cont, {:ok, [tuple | acc]}} + end + end) + |> case do + {:ok, formatted_tokens} -> formatted_tokens + _ -> [] + end + end +end diff --git a/apps/language_server/lib/language_server/providers/folding_range/token_pairs.ex b/apps/language_server/lib/language_server/providers/folding_range/token_pairs.ex index 5c720e2ce..82bdede99 100644 --- a/apps/language_server/lib/language_server/providers/folding_range/token_pairs.ex +++ b/apps/language_server/lib/language_server/providers/folding_range/token_pairs.ex @@ -2,19 +2,24 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRange.TokenPairs do @moduledoc """ """ - @basic_pairs %{ - do: :end, - "(": :")", - "[": :"]", - "{": :"}" - } + alias ElixirLS.LanguageServer.Providers.FoldingRange - @heredoc_pairs %{ - bin_heredoc: :eol + @token_pairs %{ + "(": [:")"], + "[": [:"]"], + "{": [:"}"], + do: [:catch, :rescue, :after, :else, :end], + catch: [:rescue, :after, :else, :end], + rescue: [:after, :else, :end], + after: [:else, :end], + else: [:end], + with: [:do], + fn: [:end] } - def provide_ranges(tokens_3_tuple) do - ranges = fold_tokens_into_ranges(tokens_3_tuple) + @spec provide_ranges([FoldingRange.Token.t()]) :: [FoldingRange.t()] + def provide_ranges(formatted_tokens) do + ranges = fold_tokens_into_ranges(formatted_tokens) {:ok, ranges} end @@ -23,26 +28,9 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRange.TokenPairs do # startLines but different endLines. # It's not clear if that case is actually a problem. defp fold_tokens_into_ranges(tokens) when is_list(tokens) do - ranges_from_pairs = tokens |> pair_tokens(@basic_pairs) - ranges_from_heredocs = tokens |> pair_tokens(@heredoc_pairs) - ranges = ranges_from_pairs ++ ranges_from_heredocs - ranges |> convert_to_spec_ranges() - end - - defp convert_to_spec_ranges(ranges) do - ranges - |> Enum.filter(fn {start_line, end_line} -> end_line > start_line end) - |> Enum.sort() - |> Enum.dedup() - ## Remove the above sort + dedup lines and uncomment the following if no - ## two ranges may share a startLine - # |> Enum.group_by(fn {start_line, _} -> start_line end) - # |> Enum.map(fn {_, ranges} -> - # Enum.max_by(ranges, fn {_, end_line} -> end_line end) - # end) - |> Enum.map(fn {start_line, end_line} -> - %{startLine: start_line, endLine: end_line} - end) + tokens + |> pair_tokens(@token_pairs) + |> convert_to_spec_ranges() end defp pair_tokens(tokens, kind_map) do @@ -66,29 +54,45 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRange.TokenPairs do # return an error if not all tokens could be paired. defp do_pair_tokens([], _stack, pairs, _kind_map), do: pairs - defp do_pair_tokens([{start_kind, _, _} = start | tail_tokens], [], pairs, kind_map) do - new_stack = if Map.get(kind_map, start_kind), do: [start], else: [] + defp do_pair_tokens([{head_kind, _, _} = head | tail_tokens], [], pairs, kind_map) do + new_stack = if kind_map |> Map.has_key?(head_kind), do: [head], else: [] do_pair_tokens(tail_tokens, new_stack, pairs, kind_map) end defp do_pair_tokens( - [{start_kind, _, _} = start | tail_tokens], + [{head_kind, _, _} = head | tail_tokens], [{top_kind, _, _} = top | tail_stack] = stack, pairs, kind_map ) do - {new_stack, new_pairs} = - cond do - Map.get(kind_map, top_kind) == start_kind -> - {tail_stack, [{top, start} | pairs]} + head_matches_any? = kind_map |> Map.has_key?(head_kind) + # Map.get/2 will always succeed because we only push matches to the stack. + head_matches_top? = kind_map |> Map.get(top_kind) |> Enum.member?(head_kind) - Map.get(kind_map, start_kind) -> - {[start | stack], pairs} - - true -> - {stack, pairs} + {new_stack, new_pairs} = + case {head_matches_any?, head_matches_top?} do + {false, false} -> {stack, pairs} + {false, true} -> {tail_stack, [{top, head} | pairs]} + {true, false} -> {[head | stack], pairs} + {true, true} -> {[head | tail_stack], [{top, head} | pairs]} end do_pair_tokens(tail_tokens, new_stack, new_pairs, kind_map) end + + defp convert_to_spec_ranges(ranges) do + ranges + |> Enum.filter(fn {start_line, end_line} -> end_line > start_line end) + |> Enum.sort() + |> Enum.dedup() + ## Remove the above sort + dedup lines and uncomment the following if no + ## two ranges may share a startLine + # |> Enum.group_by(fn {start_line, _} -> start_line end) + # |> Enum.map(fn {_, ranges} -> + # Enum.max_by(ranges, fn {_, end_line} -> end_line end) + # end) + |> Enum.map(fn {start_line, end_line} -> + %{startLine: start_line, endLine: end_line, kind?: :region} + end) + end end diff --git a/apps/language_server/test/providers/folding_range_test.exs b/apps/language_server/test/providers/folding_range_test.exs index 2aa44498a..85ee21a56 100644 --- a/apps/language_server/test/providers/folding_range_test.exs +++ b/apps/language_server/test/providers/folding_range_test.exs @@ -144,8 +144,8 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRangeTest do end # 4 """ test "can fold 1 defmodule, 1 def", %{ranges_result: ranges_result} do - assert {:ok, _ranges} = ranges_result - # assert compare_condensed_ranges(ranges, [{0, 3}, {1, 2}]) + assert {:ok, ranges} = ranges_result + assert compare_condensed_ranges(ranges, [{0, 3}, {1, 2}]) end @tag text: """ @@ -158,8 +158,8 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRangeTest do end # 6 """ test "can fold 1 defmodule, 1 def, 1 heredoc", %{ranges_result: ranges_result} do - assert {:ok, _ranges} = ranges_result - # assert compare_condensed_ranges(ranges, [{0, 5}, {1, 4}, {2, 3}]) + assert {:ok, ranges} = ranges_result + assert compare_condensed_ranges(ranges, [{0, 5}, {1, 4}, {2, 3}]) end @tag text: """ @@ -178,8 +178,8 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRangeTest do end # 12 """ test "can fold 1 defmodule, 1 complex def", %{ranges_result: ranges_result} do - assert {:ok, _ranges} = ranges_result - # assert compare_condensed_ranges(ranges, [{0, 8}, {1, 7}, {4, 6}]) + assert {:ok, ranges} = ranges_result + assert compare_condensed_ranges(ranges, [{0, 11}, {1, 10}, {4, 9}]) end @tag text: """ @@ -192,8 +192,8 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRangeTest do end # 6 """ test "can fold 2 defmodules in the top-level of file", %{ranges_result: ranges_result} do - assert {:ok, _ranges} = ranges_result - # assert compare_condensed_ranges(ranges, [{0, 1}, {4, 5}]) + assert {:ok, ranges} = ranges_result + assert compare_condensed_ranges(ranges, [{0, 1}, {4, 5}]) end @tag text: """ @@ -209,8 +209,8 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRangeTest do end # 9 """ test "can fold 1 defmodule, 1 def, 1 list", %{ranges_result: ranges_result} do - assert {:ok, _ranges} = ranges_result - # assert compare_condensed_ranges(ranges, [{0, 8}, {1, 7}, {2, 4}]) + assert {:ok, ranges} = ranges_result + assert compare_condensed_ranges(ranges, [{0, 8}, {1, 7}, {2, 4}]) end @tag text: """ @@ -237,25 +237,20 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRangeTest do end # 20 """ test "can fold heredoc w/ closing paren", %{ranges_result: ranges_result} do - assert {:ok, _ranges} = ranges_result - # ranges |> IO.inspect() - # assert compare_condensed_ranges(ranges, [{0, 8}, {1, 7}, {2, 4}]) + assert {:ok, ranges} = ranges_result + assert compare_condensed_ranges(ranges, [{0, 8}, {1, 7}, {2, 4}]) end end defp fold_text(%{text: text} = context) do - "" |> IO.puts() - text |> IO.puts() + # "" |> IO.puts() + # text |> IO.puts() ranges_result = %{text: text} |> FoldingRange.provide() {:ok, Map.put(context, :ranges_result, ranges_result)} end - # defp compare_condensed_ranges(result, condensed_expected) do - # condensed_result = result |> Enum.map(&condense_range/1) - # assert condensed_result == condensed_expected - # end - - # defp condense_range(range) do - # {range["startLine"], range["endLine"]} - # end + defp compare_condensed_ranges(result, condensed_expected) do + condensed_result = result |> Enum.map(&{&1.startLine, &1.endLine}) + assert condensed_result == condensed_expected + end end From 5b876de467bf9669a93a5ee85284e01ab81d70fe Mon Sep 17 00:00:00 2001 From: William Lanchantin Date: Tue, 16 Feb 2021 13:12:31 -0500 Subject: [PATCH 20/61] fix example --- .../lib/language_server/providers/folding_range.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/language_server/lib/language_server/providers/folding_range.ex b/apps/language_server/lib/language_server/providers/folding_range.ex index 0cd993ebc..48367304e 100644 --- a/apps/language_server/lib/language_server/providers/folding_range.ex +++ b/apps/language_server/lib/language_server/providers/folding_range.ex @@ -36,8 +36,8 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRange do ranges # [ - # %{"startLine" => 0, "endLine" => 3}, - # %{"startLine" => 1, "endLine" => 2} + # %{startLine: 0, endLine: 3}, + # %{startLine: 1, endLine: 2} # ] """ @spec provide(%{text: String.t()}) :: {:ok, [t()]} | {:error, String.t()} From a40aea49ce772521b6c18ef009b1384db737cd74 Mon Sep 17 00:00:00 2001 From: William Lanchantin Date: Tue, 16 Feb 2021 13:28:01 -0500 Subject: [PATCH 21/61] refactor indentation tests --- .../test/providers/folding_range_test.exs | 186 +++++++----------- 1 file changed, 73 insertions(+), 113 deletions(-) diff --git a/apps/language_server/test/providers/folding_range_test.exs b/apps/language_server/test/providers/folding_range_test.exs index 85ee21a56..3d54717f9 100644 --- a/apps/language_server/test/providers/folding_range_test.exs +++ b/apps/language_server/test/providers/folding_range_test.exs @@ -8,128 +8,88 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRangeTest do end describe "indentation" do - setup [:pair_cells] + setup [:fold_via_indentation] - # defmodule A do # 0 - # def hello() do # 1 - # :world # 2 - # end # 3 - # end # 4 - @tag cells: [{0, 0}, {1, 2}, {2, 4}, {3, 2}, {4, 0}] - test "basic indentation test", %{pairs: pairs} do - assert pairs == [{{0, 0}, {4, 0}}, {{1, 2}, {3, 2}}] + @tag text: """ + defmodule A do # 0 + def hello() do # 1 + :world # 2 + end # 3 + end # 4 + """ + test "basic indentation test", %{ranges_result: ranges_result} do + assert {:ok, ranges} = ranges_result + assert compare_condensed_ranges(ranges, [{0, 3}, {1, 2}]) end - # defmodule A do # 0 - # def hello() do # 1 - # # world # 2 - # if true do # 3 - # :world # 4 - # end # 5 - # end # 6 - # end # 7 - @tag cells: [{0, 0}, {1, 2}, {2, 4}, {3, 4}, {4, 6}, {5, 4}, {6, 2}, {7, 0}] - test "indent w/ successive matching levels", %{pairs: pairs} do - assert pairs == [{{0, 0}, {7, 0}}, {{1, 2}, {6, 2}}, {{3, 4}, {5, 4}}] + @tag text: """ + defmodule A do # 0 + def hello() do # 1 + # world # 2 + if true do # 3 + :world # 4 + end # 5 + end # 6 + end # 7 + """ + test "indent w/ successive matching levels", %{ranges_result: ranges_result} do + assert {:ok, ranges} = ranges_result + assert compare_condensed_ranges(ranges, [{0, 6}, {1, 5}, {3, 4}]) end - # defmodule A do # 0 - # def f(%{"key" => value} = map) do # 1 - # case NaiveDateTime.from_iso8601(value) do # 2 - # {:ok, ndt} -> # 3 - # dt = # 4 - # ndt # 5 - # |> DateTime.from_naive!("Etc/UTC") # 6 - # |> Map.put(:microsecond, {0, 6}) # 7 - # # 8 - # %{map | "key" => dt} # 9 - # # 10 - # e -> # 11 - # Logger.warn(""" # 12 - # Could not use data map from #{inspect(value)} # 13 - # #{inspect(e)} # 14 - # """) # 15 - # # 16 - # :could_not_parse_value # 17 - # end # 18 - # end # 19 - # end # 20 - @tag cells: [ - {0, 0}, - {1, 2}, - {2, 4}, - {3, 6}, - {4, 8}, - {5, 10}, - {6, 10}, - {7, 10}, - {8, nil}, - {9, 8}, - {10, nil}, - {11, 6}, - {12, 8}, - {13, 8}, - {14, 8}, - {15, 8}, - {16, nil}, - {17, 8}, - {18, 4}, - {19, 2}, - {20, 0} - ] - test "indent w/ complicated function", %{pairs: pairs} do - assert pairs == [ - {{0, 0}, {20, 0}}, - {{1, 2}, {19, 2}}, - {{2, 4}, {18, 4}}, - {{3, 6}, {10, nil}}, - {{4, 8}, {8, nil}}, - {{11, 6}, {18, 4}} - ] + @tag text: """ + defmodule A do # 0 + def f(%{"key" => value} = map) do # 1 + case NaiveDateTime.from_iso8601(value) do # 2 + {:ok, ndt} -> # 3 + dt = # 4 + ndt # 5 + |> DateTime.from_naive!("Etc/UTC") # 6 + |> Map.put(:microsecond, {0, 6}) # 7 + + %{map | "key" => dt} # 9 + + e -> # 11 + Logger.warn(\"\"\" + Could not use data map from #\{inspect(value)\} # 13 + #\{inspect(e)\} # 14 + \"\"\") + + :could_not_parse_value # 17 + end # 18 + end # 19 + end # 20 + """ + test "indent w/ complicated function", %{ranges_result: ranges_result} do + assert {:ok, ranges} = ranges_result + expected = [{0, 19}, {1, 18}, {2, 17}, {3, 9}, {4, 7}, {11, 17}] + assert compare_condensed_ranges(ranges, expected) end - # defmodule A do # 0 - # def get_info(args) do # 1 - # org = # 2 - # args # 3 - # |> Ecto.assoc(:organization) # 4 - # |> Repo.one!() # 5 - # # 6 - # user = # 7 - # org # 8 - # |> Organization.user!() # 9 - # # 10 - # {:ok, %{org: org, user: user}} # 11 - # end # 12 - # end # 13 - @tag cells: [ - {0, 0}, - {1, 2}, - {2, 4}, - {3, 6}, - {4, 6}, - {5, 6}, - {6, nil}, - {7, 4}, - {8, 6}, - {9, 6}, - {10, nil}, - {11, 4}, - {12, 2}, - {13, 0} - ] - test "indent w/ different complicated function", %{pairs: pairs} do - assert pairs == [ - {{0, 0}, {13, 0}}, - {{1, 2}, {12, 2}}, - {{2, 4}, {6, nil}}, - {{7, 4}, {10, nil}} - ] + @tag text: """ + defmodule A do # 0 + def get_info(args) do # 1 + org = # 2 + args # 3 + |> Ecto.assoc(:organization) # 4 + |> Repo.one!() # 5 + + user = # 7 + org # 8 + |> Organization.user!() # 9 + + {:ok, %{org: org, user: user}} # 11 + end # 12 + end # 13 + """ + test "indent w/ different complicated function", %{ranges_result: ranges_result} do + assert {:ok, ranges} = ranges_result + assert compare_condensed_ranges(ranges, [{0, 12}, {1, 11}, {2, 5}, {7, 9}]) end - defp pair_cells(%{cells: cells} = context) do - pairs = FoldingRange.Indentation.pair_cells(cells) - {:ok, Map.put(context, :pairs, pairs)} + defp fold_via_indentation(%{text: text} = context) do + ranges_result = text |> FoldingRange.Indentation.provide_ranges() + {:ok, Map.put(context, :ranges_result, ranges_result)} end end From 593b2c3227a174460569547d1a39b50d4eefbf3c Mon Sep 17 00:00:00 2001 From: William Lanchantin Date: Tue, 16 Feb 2021 13:29:12 -0500 Subject: [PATCH 22/61] refactor indentation tests --- apps/language_server/test/providers/folding_range_test.exs | 2 -- 1 file changed, 2 deletions(-) diff --git a/apps/language_server/test/providers/folding_range_test.exs b/apps/language_server/test/providers/folding_range_test.exs index 3d54717f9..91b7290fe 100644 --- a/apps/language_server/test/providers/folding_range_test.exs +++ b/apps/language_server/test/providers/folding_range_test.exs @@ -203,8 +203,6 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRangeTest do end defp fold_text(%{text: text} = context) do - # "" |> IO.puts() - # text |> IO.puts() ranges_result = %{text: text} |> FoldingRange.provide() {:ok, Map.put(context, :ranges_result, ranges_result)} end From f39848f60d55fd001b2d7d56a0e31dae8ed90664 Mon Sep 17 00:00:00 2001 From: William Lanchantin Date: Tue, 16 Feb 2021 13:33:06 -0500 Subject: [PATCH 23/61] remove unused function --- .../providers/folding_range.ex | 27 ------------------- 1 file changed, 27 deletions(-) diff --git a/apps/language_server/lib/language_server/providers/folding_range.ex b/apps/language_server/lib/language_server/providers/folding_range.ex index 48367304e..1c99f5ecf 100644 --- a/apps/language_server/lib/language_server/providers/folding_range.ex +++ b/apps/language_server/lib/language_server/providers/folding_range.ex @@ -58,33 +58,6 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRange do {:ok, ranges} end - # Make pattern-matching easier by forcing all tuples to be 3-tuples. - # Also convert to 0-indexing as ranges are 0-indexed. - defp format_tokens(reversed_tokens) when is_list(reversed_tokens) do - reversed_tokens - # This reverses the tokens, but they come out of Tokenizer.tokenize/1 - # already reversed. - |> Enum.reduce_while({:ok, []}, fn tuple, {:ok, acc} -> - tuple = - case tuple do - {a, {b1, b2, b3}} -> {a, {b1 - 1, b2 - 1, b3}, nil} - {a, {b1, b2, b3}, c} -> {a, {b1 - 1, b2 - 1, b3}, c} - # raise here? - _ -> :error - end - - if tuple == :error do - {:halt, :error} - else - {:cont, {:ok, [tuple | acc]}} - end - end) - |> case do - {:ok, formatted_tokens} -> formatted_tokens - _ -> [] - end - end - defp merge_ranges(list_of_range_lists) do list_of_range_lists end From 9bbf34d9ce92f4665e1a2d1935f2d9ad633dd074 Mon Sep 17 00:00:00 2001 From: William Lanchantin Date: Tue, 16 Feb 2021 13:35:54 -0500 Subject: [PATCH 24/61] passing tests! --- .../test/providers/folding_range_test.exs | 27 +++++-------------- 1 file changed, 7 insertions(+), 20 deletions(-) diff --git a/apps/language_server/test/providers/folding_range_test.exs b/apps/language_server/test/providers/folding_range_test.exs index 91b7290fe..08c359c07 100644 --- a/apps/language_server/test/providers/folding_range_test.exs +++ b/apps/language_server/test/providers/folding_range_test.exs @@ -94,7 +94,7 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRangeTest do end describe "genuine source files" do - setup [:fold_text] + setup [:fold_via_token_pairs] @tag text: """ defmodule A do # 0 @@ -108,20 +108,6 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRangeTest do assert compare_condensed_ranges(ranges, [{0, 3}, {1, 2}]) end - @tag text: """ - defmodule A do # 0 - def hello() do # 1 - \"\"\" - hello # 3 - \"\"\" - end # 5 - end # 6 - """ - test "can fold 1 defmodule, 1 def, 1 heredoc", %{ranges_result: ranges_result} do - assert {:ok, ranges} = ranges_result - assert compare_condensed_ranges(ranges, [{0, 5}, {1, 4}, {2, 3}]) - end - @tag text: """ defmodule A do # 0 def hello() do # 1 @@ -198,13 +184,14 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRangeTest do """ test "can fold heredoc w/ closing paren", %{ranges_result: ranges_result} do assert {:ok, ranges} = ranges_result - assert compare_condensed_ranges(ranges, [{0, 8}, {1, 7}, {2, 4}]) + assert compare_condensed_ranges(ranges, [{0, 19}, {1, 18}, {2, 17}, {12, 14}]) end - end - defp fold_text(%{text: text} = context) do - ranges_result = %{text: text} |> FoldingRange.provide() - {:ok, Map.put(context, :ranges_result, ranges_result)} + defp fold_via_token_pairs(%{text: text} = context) do + formatted_tokens = FoldingRange.Token.format_string(text) + ranges_result = formatted_tokens |> FoldingRange.TokenPairs.provide_ranges() + {:ok, Map.put(context, :ranges_result, ranges_result)} + end end defp compare_condensed_ranges(result, condensed_expected) do From dc7349ce62f49f41d31761ad7cef01ff04a8665e Mon Sep 17 00:00:00 2001 From: William Lanchantin Date: Tue, 16 Feb 2021 13:39:57 -0500 Subject: [PATCH 25/61] fix return types --- .../lib/language_server/providers/folding_range/indentation.ex | 2 +- .../lib/language_server/providers/folding_range/token_pairs.ex | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/language_server/lib/language_server/providers/folding_range/indentation.ex b/apps/language_server/lib/language_server/providers/folding_range/indentation.ex index a8c2b5d0f..d203c3c1c 100644 --- a/apps/language_server/lib/language_server/providers/folding_range/indentation.ex +++ b/apps/language_server/lib/language_server/providers/folding_range/indentation.ex @@ -11,7 +11,7 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRange.Indentation do Provides ranges for the source text based on the indentation level. Note that we trim trailing empy rows from regions. """ - @spec provide_ranges(String.t()) :: [FoldingRange.t()] + @spec provide_ranges(String.t()) :: {:ok, [FoldingRange.t()]} def provide_ranges(text) do ranges = text diff --git a/apps/language_server/lib/language_server/providers/folding_range/token_pairs.ex b/apps/language_server/lib/language_server/providers/folding_range/token_pairs.ex index 82bdede99..44860c700 100644 --- a/apps/language_server/lib/language_server/providers/folding_range/token_pairs.ex +++ b/apps/language_server/lib/language_server/providers/folding_range/token_pairs.ex @@ -17,7 +17,7 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRange.TokenPairs do fn: [:end] } - @spec provide_ranges([FoldingRange.Token.t()]) :: [FoldingRange.t()] + @spec provide_ranges([FoldingRange.Token.t()]) :: {:ok, [FoldingRange.t()]} def provide_ranges(formatted_tokens) do ranges = fold_tokens_into_ranges(formatted_tokens) {:ok, ranges} From ef95266e5ffbd215f3f423f8b7c1561ee205075e Mon Sep 17 00:00:00 2001 From: William Lanchantin Date: Tue, 16 Feb 2021 14:08:24 -0500 Subject: [PATCH 26/61] add unusual indentation and end-to-end tests --- .../test/providers/folding_range_test.exs | 52 ++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/apps/language_server/test/providers/folding_range_test.exs b/apps/language_server/test/providers/folding_range_test.exs index 08c359c07..fed3fc6a7 100644 --- a/apps/language_server/test/providers/folding_range_test.exs +++ b/apps/language_server/test/providers/folding_range_test.exs @@ -93,7 +93,7 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRangeTest do end end - describe "genuine source files" do + describe "token pairs" do setup [:fold_via_token_pairs] @tag text: """ @@ -108,6 +108,18 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRangeTest do assert compare_condensed_ranges(ranges, [{0, 3}, {1, 2}]) end + @tag text: """ + defmodule A do # 0 + def hello() do # 1 + :world # 2 + end # 3 + end # 4 + """ + test "unusual indentation", %{ranges_result: ranges_result} do + assert {:ok, ranges} = ranges_result + assert compare_condensed_ranges(ranges, [{0, 3}, {1, 2}]) + end + @tag text: """ defmodule A do # 0 def hello() do # 1 @@ -194,6 +206,44 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRangeTest do end end + describe "end to end" do + setup [:fold_text] + + @tag text: """ + defmodule A do # 0 + def f(%{"key" => value} = map) do # 1 + case NaiveDateTime.from_iso8601(value) do # 2 + {:ok, ndt} -> # 3 + dt = # 4 + ndt # 5 + |> DateTime.from_naive!("Etc/UTC") # 6 + |> Map.put(:microsecond, {0, 6}) # 7 + + %{map | "key" => dt} # 9 + + e -> # 11 + Logger.warn(\"\"\" + Could not use data map from #\{inspect(value)\} # 13 + #\{inspect(e)\} # 14 + \"\"\") + + :could_not_parse_value # 17 + end # 18 + end # 19 + end # 20 + """ + test "can fold heredoc w/ closing paren", %{ranges_result: ranges_result} do + assert {:ok, ranges} = ranges_result + expected = [{0, 19}, {1, 18}, {2, 17}, {3, 9}, {4, 7}, {11, 17}, {12, 14}] + assert compare_condensed_ranges(ranges, expected) + end + + defp fold_text(%{text: _text} = context) do + ranges_result = FoldingRange.provide(context) + {:ok, Map.put(context, :ranges_result, ranges_result)} + end + end + defp compare_condensed_ranges(result, condensed_expected) do condensed_result = result |> Enum.map(&{&1.startLine, &1.endLine}) assert condensed_result == condensed_expected From 61b3c2a095ae3072545d053dc4817973db7d6656 Mon Sep 17 00:00:00 2001 From: William Lanchantin Date: Tue, 16 Feb 2021 14:08:32 -0500 Subject: [PATCH 27/61] fix merging logic --- .../providers/folding_range.ex | 30 +++++++++++++++---- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/apps/language_server/lib/language_server/providers/folding_range.ex b/apps/language_server/lib/language_server/providers/folding_range.ex index 1c99f5ecf..ae9e4ffb1 100644 --- a/apps/language_server/lib/language_server/providers/folding_range.ex +++ b/apps/language_server/lib/language_server/providers/folding_range.ex @@ -52,13 +52,33 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRange do defp do_provide(text) do formatted_tokens = __MODULE__.Token.format_string(text) {:ok, token_pair_ranges} = formatted_tokens |> __MODULE__.TokenPairs.provide_ranges() - # {:ok, indentation_ranges} = text |> __MODULE__.Indentation.provide_ranges() - indentation_ranges = [] - ranges = merge_ranges(token_pair_ranges ++ indentation_ranges) + {:ok, indentation_ranges} = text |> __MODULE__.Indentation.provide_ranges() + + ranges = + merge_ranges_with_priorities([ + {1, indentation_ranges}, + {2, token_pair_ranges} + ]) + {:ok, ranges} end - defp merge_ranges(list_of_range_lists) do - list_of_range_lists + defp merge_ranges_with_priorities(range_lists_with_priorities) do + range_lists_with_priorities + |> Enum.reduce(%{}, fn {priority, ranges}, acc -> + ranges + |> Enum.reduce(acc, fn %{startLine: start} = range, acc -> + ranges_with_priority = Map.get(acc, start, []) + Map.put(acc, start, [{priority, range} | ranges_with_priority]) + end) + end) + |> Enum.map(fn {_start, ranges_with_priority} -> + {_priority, range} = + ranges_with_priority + |> Enum.max_by(fn {priority, range} -> {priority, range.endLine} end) + + range + end) + |> Enum.sort_by(& &1.startLine) end end From 64d495b5658afb8c36c2586ca20ca40cc3a14153 Mon Sep 17 00:00:00 2001 From: William Lanchantin Date: Tue, 16 Feb 2021 14:13:36 -0500 Subject: [PATCH 28/61] update test names --- .../test/providers/folding_range_test.exs | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/apps/language_server/test/providers/folding_range_test.exs b/apps/language_server/test/providers/folding_range_test.exs index fed3fc6a7..c147a7b2d 100644 --- a/apps/language_server/test/providers/folding_range_test.exs +++ b/apps/language_server/test/providers/folding_range_test.exs @@ -17,7 +17,7 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRangeTest do end # 3 end # 4 """ - test "basic indentation test", %{ranges_result: ranges_result} do + test "basic test", %{ranges_result: ranges_result} do assert {:ok, ranges} = ranges_result assert compare_condensed_ranges(ranges, [{0, 3}, {1, 2}]) end @@ -32,7 +32,7 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRangeTest do end # 6 end # 7 """ - test "indent w/ successive matching levels", %{ranges_result: ranges_result} do + test "consecutive matching levels", %{ranges_result: ranges_result} do assert {:ok, ranges} = ranges_result assert compare_condensed_ranges(ranges, [{0, 6}, {1, 5}, {3, 4}]) end @@ -60,7 +60,7 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRangeTest do end # 19 end # 20 """ - test "indent w/ complicated function", %{ranges_result: ranges_result} do + test "complicated function", %{ranges_result: ranges_result} do assert {:ok, ranges} = ranges_result expected = [{0, 19}, {1, 18}, {2, 17}, {3, 9}, {4, 7}, {11, 17}] assert compare_condensed_ranges(ranges, expected) @@ -82,7 +82,7 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRangeTest do end # 12 end # 13 """ - test "indent w/ different complicated function", %{ranges_result: ranges_result} do + test "different complicated function", %{ranges_result: ranges_result} do assert {:ok, ranges} = ranges_result assert compare_condensed_ranges(ranges, [{0, 12}, {1, 11}, {2, 5}, {7, 9}]) end @@ -103,7 +103,7 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRangeTest do end # 3 end # 4 """ - test "can fold 1 defmodule, 1 def", %{ranges_result: ranges_result} do + test "basic test", %{ranges_result: ranges_result} do assert {:ok, ranges} = ranges_result assert compare_condensed_ranges(ranges, [{0, 3}, {1, 2}]) end @@ -135,7 +135,7 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRangeTest do end # 11 end # 12 """ - test "can fold 1 defmodule, 1 complex def", %{ranges_result: ranges_result} do + test "1 defmodule, 1 def, 1 case", %{ranges_result: ranges_result} do assert {:ok, ranges} = ranges_result assert compare_condensed_ranges(ranges, [{0, 11}, {1, 10}, {4, 9}]) end @@ -149,7 +149,7 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRangeTest do @moduledoc "This is module B" # 5 end # 6 """ - test "can fold 2 defmodules in the top-level of file", %{ranges_result: ranges_result} do + test "2 defmodules in the top-level of file", %{ranges_result: ranges_result} do assert {:ok, ranges} = ranges_result assert compare_condensed_ranges(ranges, [{0, 1}, {4, 5}]) end @@ -166,7 +166,7 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRangeTest do end # 8 end # 9 """ - test "can fold 1 defmodule, 1 def, 1 list", %{ranges_result: ranges_result} do + test "1 defmodule, 1 def, 1 list", %{ranges_result: ranges_result} do assert {:ok, ranges} = ranges_result assert compare_condensed_ranges(ranges, [{0, 8}, {1, 7}, {2, 4}]) end @@ -194,7 +194,7 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRangeTest do end # 19 end # 20 """ - test "can fold heredoc w/ closing paren", %{ranges_result: ranges_result} do + test "complicated function", %{ranges_result: ranges_result} do assert {:ok, ranges} = ranges_result assert compare_condensed_ranges(ranges, [{0, 19}, {1, 18}, {2, 17}, {12, 14}]) end @@ -232,7 +232,7 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRangeTest do end # 19 end # 20 """ - test "can fold heredoc w/ closing paren", %{ranges_result: ranges_result} do + test "complicated function", %{ranges_result: ranges_result} do assert {:ok, ranges} = ranges_result expected = [{0, 19}, {1, 18}, {2, 17}, {3, 9}, {4, 7}, {11, 17}, {12, 14}] assert compare_condensed_ranges(ranges, expected) From 2409d8a4a4295a9c4a3eaf8723d7a007b43042b8 Mon Sep 17 00:00:00 2001 From: William Lanchantin Date: Tue, 16 Feb 2021 14:28:49 -0500 Subject: [PATCH 29/61] remove todo --- .../lib/language_server/providers/folding_range.ex | 5 ----- 1 file changed, 5 deletions(-) diff --git a/apps/language_server/lib/language_server/providers/folding_range.ex b/apps/language_server/lib/language_server/providers/folding_range.ex index ae9e4ffb1..9426d5cc3 100644 --- a/apps/language_server/lib/language_server/providers/folding_range.ex +++ b/apps/language_server/lib/language_server/providers/folding_range.ex @@ -4,11 +4,6 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRange do See specification here: https://microsoft.github.io/language-server-protocol/specifications/specification-3-15/#textDocument_foldingRange - - ## TODO - - - [x] Indentation pass - - [ ] Add priorities and do a proper merge """ @type t :: %{ From 5304e2517c89a5eb0e958f1c0faf2b960d7599de Mon Sep 17 00:00:00 2001 From: William Lanchantin Date: Thu, 18 Feb 2021 11:53:11 -0500 Subject: [PATCH 30/61] add heredoc support --- .../providers/folding_range.ex | 4 +- .../providers/folding_range/heredoc.ex | 64 +++++++++++++++ .../test/providers/folding_range_test.exs | 80 +++++++++++++++---- 3 files changed, 131 insertions(+), 17 deletions(-) create mode 100644 apps/language_server/lib/language_server/providers/folding_range/heredoc.ex diff --git a/apps/language_server/lib/language_server/providers/folding_range.ex b/apps/language_server/lib/language_server/providers/folding_range.ex index 9426d5cc3..bc4500612 100644 --- a/apps/language_server/lib/language_server/providers/folding_range.ex +++ b/apps/language_server/lib/language_server/providers/folding_range.ex @@ -48,11 +48,13 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRange do formatted_tokens = __MODULE__.Token.format_string(text) {:ok, token_pair_ranges} = formatted_tokens |> __MODULE__.TokenPairs.provide_ranges() {:ok, indentation_ranges} = text |> __MODULE__.Indentation.provide_ranges() + {:ok, heredoc_ranges} = formatted_tokens |> __MODULE__.Heredoc.provide_ranges() ranges = merge_ranges_with_priorities([ {1, indentation_ranges}, - {2, token_pair_ranges} + {2, token_pair_ranges}, + {2, heredoc_ranges} ]) {:ok, ranges} diff --git a/apps/language_server/lib/language_server/providers/folding_range/heredoc.ex b/apps/language_server/lib/language_server/providers/folding_range/heredoc.ex new file mode 100644 index 000000000..97e5a42d9 --- /dev/null +++ b/apps/language_server/lib/language_server/providers/folding_range/heredoc.ex @@ -0,0 +1,64 @@ +defmodule ElixirLS.LanguageServer.Providers.FoldingRange.Heredoc do + @moduledoc """ + """ + + alias ElixirLS.LanguageServer.Providers.FoldingRange + + @spec provide_ranges([FoldingRange.Token.t()]) :: {:ok, [FoldingRange.t()]} + def provide_ranges(formatted_tokens) do + ranges = + formatted_tokens + |> group_heredoc_tokens() + |> convert_heredoc_groups_to_ranges() + |> Enum.sort_by(& &1.startLine) + + {:ok, ranges} + end + + # The :bin_heredoc token will be either + # - by itself or + # - directly after an :identifier (either :doc or :moduledoc). + # :bin_heredoc regions are ended by an :eol token. + defp group_heredoc_tokens(tokens) do + tokens + |> Enum.reduce([], fn + {:identifier, _, x} = token, acc when x in [:doc, :moduledoc] -> + [[token] | acc] + + {:bin_heredoc, _, _} = token, [[{:identifier, _, _}] = head | tail] -> + [[token | head] | tail] + + {:bin_heredoc, _, _} = token, acc -> + [[token] | acc] + + {:eol, _, _} = token, [[{:bin_heredoc, _, _} | _] = head | tail] -> + [[token | head] | tail] + + _, acc -> + acc + end) + end + + defp convert_heredoc_groups_to_ranges(heredoc_groups) do + heredoc_groups + |> Enum.map(&first_and_last_of_list/1) + |> Enum.map(fn {last, first} -> classify_group(first, last) end) + end + + defp classify_group({:bin_heredoc, {start_line, _, _}, _}, {_, {end_line, _, _}, _}) do + %{startLine: start_line, endLine: end_line - 1, kind?: :region} + end + + defp classify_group({:identifier, {start_line, _, _}, _}, {_, {end_line, _, _}, _}) do + %{startLine: start_line, endLine: end_line - 1, kind?: :comment} + end + + defp first_and_last_of_list(list) when is_list(list) do + [head | tail] = list + do_falo_list(tail, head) + end + + defp do_falo_list([], first), do: {first, first} + defp do_falo_list([last | []], first), do: {first, last} + defp do_falo_list([_ | tail], first), do: do_falo_list(tail, first) +end diff --git a/apps/language_server/test/providers/folding_range_test.exs b/apps/language_server/test/providers/folding_range_test.exs index c147a7b2d..cc9a16f18 100644 --- a/apps/language_server/test/providers/folding_range_test.exs +++ b/apps/language_server/test/providers/folding_range_test.exs @@ -206,35 +206,83 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRangeTest do end end + describe "heredocs" do + setup [:fold_via_heredocs] + + @tag text: """ + defmodule A do # 0 + @moduledoc \"\"\" + I'm a @moduledoc heredoc. # 2 + \"\"\" + + @doc \"\"\" + I'm a @doc heredoc. # 6 + \"\"\" + def f(%{"key" => value} = map) do # 8 + case NaiveDateTime.from_iso8601(value) do # 9 + {:ok, ndt} -> # 10 + dt = # 11 + ndt # 12 + |> DateTime.from_naive!("Etc/UTC") # 13 + |> Map.put(:microsecond, {0, 6}) # 14 + + %{map | "key" => dt} # 16 + + e -> # 18 + Logger.warn(\"\"\" + I'm a regular heredoc # 20 + \"\"\") + + :could_not_parse_value # 23 + end # 24 + end # 25 + end # 26 + """ + test "@moduledoc, @doc, and stand-alone heredocs", %{ranges_result: ranges_result} do + assert {:ok, ranges} = ranges_result + assert compare_condensed_ranges(ranges, [{1, 2}, {5, 6}, {19, 20}]) + end + + defp fold_via_heredocs(%{text: text} = context) do + formatted_tokens = FoldingRange.Token.format_string(text) + ranges_result = formatted_tokens |> FoldingRange.Heredoc.provide_ranges() + {:ok, Map.put(context, :ranges_result, ranges_result)} + end + end + describe "end to end" do setup [:fold_text] @tag text: """ defmodule A do # 0 - def f(%{"key" => value} = map) do # 1 - case NaiveDateTime.from_iso8601(value) do # 2 - {:ok, ndt} -> # 3 - dt = # 4 - ndt # 5 - |> DateTime.from_naive!("Etc/UTC") # 6 - |> Map.put(:microsecond, {0, 6}) # 7 + @moduledoc \"\"\" + I'm a @moduledoc heredoc. # 2 + \"\"\" - %{map | "key" => dt} # 9 + def f(%{"key" => value} = map) do # 5 + case NaiveDateTime.from_iso8601(value) do # 6 + {:ok, ndt} -> # 7 + dt = # 8 + ndt # 9 + |> DateTime.from_naive!("Etc/UTC") # 10 + |> Map.put(:microsecond, {0, 6}) # 11 - e -> # 11 + %{map | "key" => dt} # 13 + + e -> # 15 Logger.warn(\"\"\" - Could not use data map from #\{inspect(value)\} # 13 - #\{inspect(e)\} # 14 + Could not use data map from #\{inspect(value)\} # 17 + #\{inspect(e)\} # 18 \"\"\") - :could_not_parse_value # 17 - end # 18 - end # 19 - end # 20 + :could_not_parse_value # 21 + end # 22 + end # 23 + end # 24 """ test "complicated function", %{ranges_result: ranges_result} do assert {:ok, ranges} = ranges_result - expected = [{0, 19}, {1, 18}, {2, 17}, {3, 9}, {4, 7}, {11, 17}, {12, 14}] + expected = [{0, 23}, {1, 2}, {5, 22}, {6, 21}, {7, 13}, {8, 11}, {15, 21}, {16, 18}] assert compare_condensed_ranges(ranges, expected) end From f39acb91f8a277a46376351f85a6f9bd4a906b66 Mon Sep 17 00:00:00 2001 From: William Lanchantin Date: Thu, 18 Feb 2021 12:38:03 -0500 Subject: [PATCH 31/61] create a unified input --- .../providers/folding_range.ex | 22 +++++++-- .../providers/folding_range/heredoc.ex | 6 +-- .../providers/folding_range/indentation.ex | 32 ++++--------- .../providers/folding_range/line.ex | 47 +++++++++++++++++++ .../providers/folding_range/token_pairs.ex | 18 ++++--- .../test/providers/folding_range_test.exs | 20 ++++++-- 6 files changed, 99 insertions(+), 46 deletions(-) create mode 100644 apps/language_server/lib/language_server/providers/folding_range/line.ex diff --git a/apps/language_server/lib/language_server/providers/folding_range.ex b/apps/language_server/lib/language_server/providers/folding_range.ex index bc4500612..d3b8db49b 100644 --- a/apps/language_server/lib/language_server/providers/folding_range.ex +++ b/apps/language_server/lib/language_server/providers/folding_range.ex @@ -6,6 +6,13 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRange do https://microsoft.github.io/language-server-protocol/specifications/specification-3-15/#textDocument_foldingRange """ + alias __MODULE__ + + @type input :: %{ + tokens: [FoldingRange.Token.t()], + lines: [FoldingRange.Line.t()] + } + @type t :: %{ required(:startLine) => non_neg_integer(), required(:endLine) => non_neg_integer(), @@ -45,10 +52,10 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRange do end defp do_provide(text) do - formatted_tokens = __MODULE__.Token.format_string(text) - {:ok, token_pair_ranges} = formatted_tokens |> __MODULE__.TokenPairs.provide_ranges() - {:ok, indentation_ranges} = text |> __MODULE__.Indentation.provide_ranges() - {:ok, heredoc_ranges} = formatted_tokens |> __MODULE__.Heredoc.provide_ranges() + input = convert_text_to_input(text) + {:ok, token_pair_ranges} = input |> FoldingRange.TokenPairs.provide_ranges() + {:ok, indentation_ranges} = input |> FoldingRange.Indentation.provide_ranges() + {:ok, heredoc_ranges} = input |> FoldingRange.Heredoc.provide_ranges() ranges = merge_ranges_with_priorities([ @@ -60,6 +67,13 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRange do {:ok, ranges} end + def convert_text_to_input(text) do + %{ + tokens: FoldingRange.Token.format_string(text), + lines: FoldingRange.Line.format_string(text) + } + end + defp merge_ranges_with_priorities(range_lists_with_priorities) do range_lists_with_priorities |> Enum.reduce(%{}, fn {priority, ranges}, acc -> diff --git a/apps/language_server/lib/language_server/providers/folding_range/heredoc.ex b/apps/language_server/lib/language_server/providers/folding_range/heredoc.ex index 97e5a42d9..c940564cf 100644 --- a/apps/language_server/lib/language_server/providers/folding_range/heredoc.ex +++ b/apps/language_server/lib/language_server/providers/folding_range/heredoc.ex @@ -4,10 +4,10 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRange.Heredoc do alias ElixirLS.LanguageServer.Providers.FoldingRange - @spec provide_ranges([FoldingRange.Token.t()]) :: {:ok, [FoldingRange.t()]} - def provide_ranges(formatted_tokens) do + @spec provide_ranges([FoldingRange.input()]) :: {:ok, [FoldingRange.t()]} + def provide_ranges(%{tokens: tokens}) do ranges = - formatted_tokens + tokens |> group_heredoc_tokens() |> convert_heredoc_groups_to_ranges() |> Enum.sort_by(& &1.startLine) diff --git a/apps/language_server/lib/language_server/providers/folding_range/indentation.ex b/apps/language_server/lib/language_server/providers/folding_range/indentation.ex index d203c3c1c..a96a0f3dd 100644 --- a/apps/language_server/lib/language_server/providers/folding_range/indentation.ex +++ b/apps/language_server/lib/language_server/providers/folding_range/indentation.ex @@ -4,46 +4,30 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRange.Indentation do """ alias ElixirLS.LanguageServer.Providers.FoldingRange - - @type cell :: {non_neg_integer(), non_neg_integer() | nil} + alias ElixirLS.LanguageServer.Providers.FoldingRange.Line @doc """ Provides ranges for the source text based on the indentation level. Note that we trim trailing empy rows from regions. """ - @spec provide_ranges(String.t()) :: {:ok, [FoldingRange.t()]} - def provide_ranges(text) do + @spec provide_ranges(FoldingRange.input()) :: {:ok, [FoldingRange.t()]} + def provide_ranges(%{lines: lines}) do ranges = - text - |> find_cells() + lines + |> Enum.map(&extract_cell/1) |> pair_cells() |> pairs_to_ranges() {:ok, ranges} end - # If we think of the code text as a grid, this function finds the cells whose - # columns are the start of each row (line). - # Empty rows are represented as {row, nil}. - @spec find_cells(String.t()) :: [cell()] - defp find_cells(text) do - text - |> String.trim() - |> String.split("\n") - |> Enum.with_index() - |> Enum.map(fn {line, row} -> - full = line |> String.length() - trimmed = line |> String.trim_leading() |> String.length() - col = if {full, trimmed} == {0, 0}, do: nil, else: full - trimmed - {row, col} - end) - end + defp extract_cell({_line, cell, _first}), do: cell @doc """ Pairs cells into {start, end} tuples of regions Public function for testing """ - @spec pair_cells([cell()]) :: [{cell(), cell()}] + @spec pair_cells([Line.cell()]) :: [{Line.cell(), Line.cell()}] def pair_cells(cells) do do_pair_cells(cells, [], [], []) end @@ -84,7 +68,7 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRange.Indentation do do_pair_cells(tail, [head | new_tail_stack], [], new_pairs ++ pairs) end - @spec pairs_to_ranges([{cell(), cell()}]) :: [FoldingRange.t()] + @spec pairs_to_ranges([{Line.cell(), Line.cell()}]) :: [FoldingRange.t()] defp pairs_to_ranges(pairs) do pairs |> Enum.map(fn {{r1, _}, {r2, _}} -> diff --git a/apps/language_server/lib/language_server/providers/folding_range/line.ex b/apps/language_server/lib/language_server/providers/folding_range/line.ex new file mode 100644 index 000000000..8e84d36a0 --- /dev/null +++ b/apps/language_server/lib/language_server/providers/folding_range/line.ex @@ -0,0 +1,47 @@ +defmodule ElixirLS.LanguageServer.Providers.FoldingRange.Line do + @moduledoc """ + FoldingRange helpers for lines. + """ + + @type cell :: {non_neg_integer(), non_neg_integer() | nil} + @type t :: {String.t(), cell(), String.t()} + + @spec format_string(String.t()) :: [cell()] + def format_string(text) do + text + |> text_to_lines() + |> embellish_lines_with_metadata() + end + + @spec embellish_lines_with_metadata(String.t()) :: [String.t()] + defp text_to_lines(text) do + text + |> String.trim() + |> String.split("\n") + end + + # If we think of the code text as a grid, this function finds the cells whose + # columns are the start of each row (line). + # Empty rows are represented as {row, nil}. + # We also grab the first character for convenience elsewhere. + @spec embellish_lines_with_metadata([String.t()]) :: [t()] + defp embellish_lines_with_metadata(lines) do + lines + |> Enum.with_index() + |> Enum.map(fn {line, row} -> + full_length = line |> String.length() + trimmed = line |> String.trim_leading() + trimmed_length = trimmed |> String.length() + first = trimmed |> String.first() + + col = + if {full_length, trimmed_length} == {0, 0} do + nil + else + full_length - trimmed_length + end + + {line, {row, col}, first} + end) + end +end diff --git a/apps/language_server/lib/language_server/providers/folding_range/token_pairs.ex b/apps/language_server/lib/language_server/providers/folding_range/token_pairs.ex index 44860c700..4d2d300a6 100644 --- a/apps/language_server/lib/language_server/providers/folding_range/token_pairs.ex +++ b/apps/language_server/lib/language_server/providers/folding_range/token_pairs.ex @@ -17,20 +17,18 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRange.TokenPairs do fn: [:end] } - @spec provide_ranges([FoldingRange.Token.t()]) :: {:ok, [FoldingRange.t()]} - def provide_ranges(formatted_tokens) do - ranges = fold_tokens_into_ranges(formatted_tokens) - {:ok, ranges} - end - # Note # This implementation allows for the possibility of 2 ranges with the same # startLines but different endLines. # It's not clear if that case is actually a problem. - defp fold_tokens_into_ranges(tokens) when is_list(tokens) do - tokens - |> pair_tokens(@token_pairs) - |> convert_to_spec_ranges() + @spec provide_ranges([FoldingRange.input()]) :: {:ok, [FoldingRange.t()]} + def provide_ranges(%{tokens: tokens}) do + ranges = + tokens + |> pair_tokens(@token_pairs) + |> convert_to_spec_ranges() + + {:ok, ranges} end defp pair_tokens(tokens, kind_map) do diff --git a/apps/language_server/test/providers/folding_range_test.exs b/apps/language_server/test/providers/folding_range_test.exs index cc9a16f18..0c3824ca9 100644 --- a/apps/language_server/test/providers/folding_range_test.exs +++ b/apps/language_server/test/providers/folding_range_test.exs @@ -88,7 +88,11 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRangeTest do end defp fold_via_indentation(%{text: text} = context) do - ranges_result = text |> FoldingRange.Indentation.provide_ranges() + ranges_result = + text + |> FoldingRange.convert_text_to_input() + |> FoldingRange.Indentation.provide_ranges() + {:ok, Map.put(context, :ranges_result, ranges_result)} end end @@ -200,8 +204,11 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRangeTest do end defp fold_via_token_pairs(%{text: text} = context) do - formatted_tokens = FoldingRange.Token.format_string(text) - ranges_result = formatted_tokens |> FoldingRange.TokenPairs.provide_ranges() + ranges_result = + text + |> FoldingRange.convert_text_to_input() + |> FoldingRange.TokenPairs.provide_ranges() + {:ok, Map.put(context, :ranges_result, ranges_result)} end end @@ -244,8 +251,11 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRangeTest do end defp fold_via_heredocs(%{text: text} = context) do - formatted_tokens = FoldingRange.Token.format_string(text) - ranges_result = formatted_tokens |> FoldingRange.Heredoc.provide_ranges() + ranges_result = + text + |> FoldingRange.convert_text_to_input() + |> FoldingRange.Heredoc.provide_ranges() + {:ok, Map.put(context, :ranges_result, ranges_result)} end end From d8666d8b669118365d43206d74147782a14c18d8 Mon Sep 17 00:00:00 2001 From: William Lanchantin Date: Thu, 18 Feb 2021 13:13:10 -0500 Subject: [PATCH 32/61] move function to helpers --- .../providers/folding_range/helpers.ex | 14 ++++++++++++++ .../providers/folding_range/heredoc.ex | 11 +---------- 2 files changed, 15 insertions(+), 10 deletions(-) create mode 100644 apps/language_server/lib/language_server/providers/folding_range/helpers.ex diff --git a/apps/language_server/lib/language_server/providers/folding_range/helpers.ex b/apps/language_server/lib/language_server/providers/folding_range/helpers.ex new file mode 100644 index 000000000..c3ef803c1 --- /dev/null +++ b/apps/language_server/lib/language_server/providers/folding_range/helpers.ex @@ -0,0 +1,14 @@ +defmodule ElixirLS.LanguageServer.Providers.FoldingRange.Helpers do + @moduledoc """ + """ + def first_and_last_of_list([]), do: :empty_list + + def first_and_last_of_list(list) when is_list(list) do + [head | tail] = list + do_falo_list(tail, head) + end + + defp do_falo_list([], first), do: {first, first} + defp do_falo_list([last | []], first), do: {first, last} + defp do_falo_list([_ | tail], first), do: do_falo_list(tail, first) +end diff --git a/apps/language_server/lib/language_server/providers/folding_range/heredoc.ex b/apps/language_server/lib/language_server/providers/folding_range/heredoc.ex index c940564cf..113895f6f 100644 --- a/apps/language_server/lib/language_server/providers/folding_range/heredoc.ex +++ b/apps/language_server/lib/language_server/providers/folding_range/heredoc.ex @@ -41,7 +41,7 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRange.Heredoc do defp convert_heredoc_groups_to_ranges(heredoc_groups) do heredoc_groups - |> Enum.map(&first_and_last_of_list/1) + |> Enum.map(&FoldingRange.Helpers.first_and_last_of_list/1) |> Enum.map(fn {last, first} -> classify_group(first, last) end) end @@ -52,13 +52,4 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRange.Heredoc do defp classify_group({:identifier, {start_line, _, _}, _}, {_, {end_line, _, _}, _}) do %{startLine: start_line, endLine: end_line - 1, kind?: :comment} end - - defp first_and_last_of_list(list) when is_list(list) do - [head | tail] = list - do_falo_list(tail, head) - end - - defp do_falo_list([], first), do: {first, first} - defp do_falo_list([last | []], first), do: {first, last} - defp do_falo_list([_ | tail], first), do: do_falo_list(tail, first) end From 2722023bd071ffbaa940a92eff9479665ce0ce5f Mon Sep 17 00:00:00 2001 From: William Lanchantin Date: Thu, 18 Feb 2021 13:13:45 -0500 Subject: [PATCH 33/61] add support for comment blocks --- .../providers/folding_range.ex | 6 +- .../providers/folding_range/comment_block.ex | 61 ++++++++++++++ .../test/providers/folding_range_test.exs | 79 +++++++++++++++---- 3 files changed, 127 insertions(+), 19 deletions(-) create mode 100644 apps/language_server/lib/language_server/providers/folding_range/comment_block.ex diff --git a/apps/language_server/lib/language_server/providers/folding_range.ex b/apps/language_server/lib/language_server/providers/folding_range.ex index d3b8db49b..70504d7b5 100644 --- a/apps/language_server/lib/language_server/providers/folding_range.ex +++ b/apps/language_server/lib/language_server/providers/folding_range.ex @@ -56,12 +56,14 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRange do {:ok, token_pair_ranges} = input |> FoldingRange.TokenPairs.provide_ranges() {:ok, indentation_ranges} = input |> FoldingRange.Indentation.provide_ranges() {:ok, heredoc_ranges} = input |> FoldingRange.Heredoc.provide_ranges() + {:ok, comment_block_ranges} = input |> FoldingRange.CommentBlock.provide_ranges() ranges = merge_ranges_with_priorities([ {1, indentation_ranges}, - {2, token_pair_ranges}, - {2, heredoc_ranges} + {2, comment_block_ranges}, + {3, token_pair_ranges}, + {3, heredoc_ranges} ]) {:ok, ranges} diff --git a/apps/language_server/lib/language_server/providers/folding_range/comment_block.ex b/apps/language_server/lib/language_server/providers/folding_range/comment_block.ex new file mode 100644 index 000000000..d1258bafe --- /dev/null +++ b/apps/language_server/lib/language_server/providers/folding_range/comment_block.ex @@ -0,0 +1,61 @@ +defmodule ElixirLS.LanguageServer.Providers.FoldingRange.CommentBlock do + @moduledoc """ + Code folding based on indentation only. + """ + + alias ElixirLS.LanguageServer.Providers.FoldingRange + alias ElixirLS.LanguageServer.Providers.FoldingRange.Line + + @doc """ + Provides ranges for the source text based on the indentation level. + Note that we trim trailing empy rows from regions. + """ + @spec provide_ranges(FoldingRange.input()) :: {:ok, [FoldingRange.t()]} + def provide_ranges(%{lines: lines}) do + ranges = + lines + |> group_comments() + |> Enum.map(&convert_comment_group_to_range/1) + + {:ok, ranges} + end + + @doc """ + Pairs cells into {start, end} tuples of regions + Public function for testing + """ + @spec group_comments([Line.t()]) :: [any()] + def group_comments(lines) do + lines + |> Enum.reduce([[]], fn + {_, cell, "#"}, [[{_, "#"} | _] = head | tail] -> + [[{cell, "#"} | head] | tail] + + {_, cell, "#"}, [[] | tail] -> + [[{cell, "#"}] | tail] + + _, [[{_, "#"} | _] | _] = acc -> + [[] | acc] + + _, acc -> + acc + end) + |> case do + [[] | groups] -> groups + groups -> groups + end + end + + defp convert_comment_group_to_range(group) do + {{{end_line, _}, _}, {{start_line, _}, _}} = + group |> FoldingRange.Helpers.first_and_last_of_list() + + %{ + startLine: start_line, + # We're not doing end_line - 1 on purpose. + # It seems weird to show the first _and_ last line of a comment block. + endLine: end_line, + kind?: :comment + } + end +end diff --git a/apps/language_server/test/providers/folding_range_test.exs b/apps/language_server/test/providers/folding_range_test.exs index 0c3824ca9..2ba6286e8 100644 --- a/apps/language_server/test/providers/folding_range_test.exs +++ b/apps/language_server/test/providers/folding_range_test.exs @@ -260,6 +260,36 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRangeTest do end end + describe "comment blocks" do + setup [:fold_via_comment_blocks] + + @tag text: """ + defmodule A do # 0 + def hello() do # 1 + do_hello() # 2 + end # 3 + + # comment block 0 # 5 + # comment block 1 # 6 + # comment block 2 # 7 + defp do_hello(), do: :world # 8 + end # 9 + """ + test "@moduledoc, @doc, and stand-alone heredocs", %{ranges_result: ranges_result} do + assert {:ok, ranges} = ranges_result + assert compare_condensed_ranges(ranges, [{5, 7}]) + end + + defp fold_via_comment_blocks(%{text: text} = context) do + ranges_result = + text + |> FoldingRange.convert_text_to_input() + |> FoldingRange.CommentBlock.provide_ranges() + + {:ok, Map.put(context, :ranges_result, ranges_result)} + end + end + describe "end to end" do setup [:fold_text] @@ -270,29 +300,44 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRangeTest do \"\"\" def f(%{"key" => value} = map) do # 5 - case NaiveDateTime.from_iso8601(value) do # 6 - {:ok, ndt} -> # 7 - dt = # 8 - ndt # 9 - |> DateTime.from_naive!("Etc/UTC") # 10 - |> Map.put(:microsecond, {0, 6}) # 11 - - %{map | "key" => dt} # 13 - - e -> # 15 + # comment block 0 # 6 + # comment block 1 # 7 + case NaiveDateTime.from_iso8601(value) do # 8 + {:ok, ndt} -> # 9 + dt = # 10 + ndt # 11 + |> DateTime.from_naive!("Etc/UTC") # 12 + |> Map.put(:microsecond, {0, 6}) # 13 + + %{map | "key" => dt} # 15 + + e -> # 17 Logger.warn(\"\"\" - Could not use data map from #\{inspect(value)\} # 17 - #\{inspect(e)\} # 18 + Could not use data map from #\{inspect(value)\} # 19 + #\{inspect(e)\} # 20 \"\"\") - :could_not_parse_value # 21 - end # 22 - end # 23 - end # 24 + :could_not_parse_value # 23 + end # 24 + end # 25 + end # 26 """ test "complicated function", %{ranges_result: ranges_result} do assert {:ok, ranges} = ranges_result - expected = [{0, 23}, {1, 2}, {5, 22}, {6, 21}, {7, 13}, {8, 11}, {15, 21}, {16, 18}] + + expected = [ + {0, 25}, + {1, 2}, + {5, 24}, + {6, 7}, + {8, 23}, + {9, 15}, + {10, 13}, + {17, 23}, + {18, 20}, + {20, 20} + ] + assert compare_condensed_ranges(ranges, expected) end From 7c52c92c110c2d92db8ae64a5c7bede5b93eb63c Mon Sep 17 00:00:00 2001 From: William Lanchantin Date: Thu, 18 Feb 2021 13:23:39 -0500 Subject: [PATCH 34/61] don't allow single line comment blocks --- .../providers/folding_range/comment_block.ex | 5 +---- .../test/providers/folding_range_test.exs | 13 +++++++++++++ 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/apps/language_server/lib/language_server/providers/folding_range/comment_block.ex b/apps/language_server/lib/language_server/providers/folding_range/comment_block.ex index d1258bafe..7d1214826 100644 --- a/apps/language_server/lib/language_server/providers/folding_range/comment_block.ex +++ b/apps/language_server/lib/language_server/providers/folding_range/comment_block.ex @@ -40,10 +40,7 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRange.CommentBlock do _, acc -> acc end) - |> case do - [[] | groups] -> groups - groups -> groups - end + |> Enum.filter(fn group -> length(group) > 1 end) end defp convert_comment_group_to_range(group) do diff --git a/apps/language_server/test/providers/folding_range_test.exs b/apps/language_server/test/providers/folding_range_test.exs index 2ba6286e8..e0d8478b4 100644 --- a/apps/language_server/test/providers/folding_range_test.exs +++ b/apps/language_server/test/providers/folding_range_test.exs @@ -263,6 +263,19 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRangeTest do describe "comment blocks" do setup [:fold_via_comment_blocks] + @tag text: """ + defmodule A do # 0 + def hello() do # 1 + # single comment # 2 + do_hello() # 3 + end # 4 + end # 5 + """ + test "no single line comment blocks", %{ranges_result: ranges_result} do + assert {:ok, ranges} = ranges_result + assert compare_condensed_ranges(ranges, []) + end + @tag text: """ defmodule A do # 0 def hello() do # 1 From dbad74fa8f757724ee825f6e497c9392bb6913f8 Mon Sep 17 00:00:00 2001 From: William Lanchantin Date: Thu, 18 Feb 2021 13:28:28 -0500 Subject: [PATCH 35/61] try not to sort multiple times; fix test --- .../lib/language_server/providers/folding_range/heredoc.ex | 1 - .../language_server/providers/folding_range/indentation.ex | 1 - apps/language_server/test/providers/folding_range_test.exs | 5 ++--- 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/apps/language_server/lib/language_server/providers/folding_range/heredoc.ex b/apps/language_server/lib/language_server/providers/folding_range/heredoc.ex index 113895f6f..b5beef442 100644 --- a/apps/language_server/lib/language_server/providers/folding_range/heredoc.ex +++ b/apps/language_server/lib/language_server/providers/folding_range/heredoc.ex @@ -10,7 +10,6 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRange.Heredoc do tokens |> group_heredoc_tokens() |> convert_heredoc_groups_to_ranges() - |> Enum.sort_by(& &1.startLine) {:ok, ranges} end diff --git a/apps/language_server/lib/language_server/providers/folding_range/indentation.ex b/apps/language_server/lib/language_server/providers/folding_range/indentation.ex index a96a0f3dd..7486728a8 100644 --- a/apps/language_server/lib/language_server/providers/folding_range/indentation.ex +++ b/apps/language_server/lib/language_server/providers/folding_range/indentation.ex @@ -40,7 +40,6 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRange.Indentation do {cell1, _, empties} -> {cell1, List.last(empties)} end) |> Enum.reject(fn {{r1, _}, {r2, _}} -> r1 + 1 >= r2 end) - |> Enum.sort() end # Empty row diff --git a/apps/language_server/test/providers/folding_range_test.exs b/apps/language_server/test/providers/folding_range_test.exs index e0d8478b4..a4ccda953 100644 --- a/apps/language_server/test/providers/folding_range_test.exs +++ b/apps/language_server/test/providers/folding_range_test.exs @@ -347,8 +347,7 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRangeTest do {9, 15}, {10, 13}, {17, 23}, - {18, 20}, - {20, 20} + {18, 20} ] assert compare_condensed_ranges(ranges, expected) @@ -362,6 +361,6 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRangeTest do defp compare_condensed_ranges(result, condensed_expected) do condensed_result = result |> Enum.map(&{&1.startLine, &1.endLine}) - assert condensed_result == condensed_expected + assert Enum.sort(condensed_result) == Enum.sort(condensed_expected) end end From 268c2036f505cb68ab387393b73bb54f8e04e79d Mon Sep 17 00:00:00 2001 From: William Lanchantin Date: Thu, 18 Feb 2021 13:28:39 -0500 Subject: [PATCH 36/61] note issue with implementation --- .../language_server/providers/folding_range/comment_block.ex | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/language_server/lib/language_server/providers/folding_range/comment_block.ex b/apps/language_server/lib/language_server/providers/folding_range/comment_block.ex index 7d1214826..be00853f2 100644 --- a/apps/language_server/lib/language_server/providers/folding_range/comment_block.ex +++ b/apps/language_server/lib/language_server/providers/folding_range/comment_block.ex @@ -1,6 +1,9 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRange.CommentBlock do @moduledoc """ - Code folding based on indentation only. + Code folding based on comment blocks. + + Note that this can create comment regions inside heredocs. + It's a little sloppy, but I don't think it's a big problem. """ alias ElixirLS.LanguageServer.Providers.FoldingRange From 6861e093842a41c669dbe6febef8d7efcfb63252 Mon Sep 17 00:00:00 2001 From: billylanchantin Date: Thu, 18 Feb 2021 14:18:18 -0500 Subject: [PATCH 37/61] include carriage returns in line-splitting logic MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Łukasz Samson --- .../lib/language_server/providers/folding_range/line.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/language_server/lib/language_server/providers/folding_range/line.ex b/apps/language_server/lib/language_server/providers/folding_range/line.ex index 8e84d36a0..4f7a32277 100644 --- a/apps/language_server/lib/language_server/providers/folding_range/line.ex +++ b/apps/language_server/lib/language_server/providers/folding_range/line.ex @@ -17,7 +17,7 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRange.Line do defp text_to_lines(text) do text |> String.trim() - |> String.split("\n") + |> String.split(["\r\n", "\n", "\r"]) end # If we think of the code text as a grid, this function finds the cells whose From 4458bdf533fa3df31f2fce97d71d26e72245ac4c Mon Sep 17 00:00:00 2001 From: William Lanchantin Date: Thu, 18 Feb 2021 14:25:54 -0500 Subject: [PATCH 38/61] use get_source_file function --- apps/language_server/lib/language_server/server.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/language_server/lib/language_server/server.ex b/apps/language_server/lib/language_server/server.ex index 28822d1c4..236d9d2d0 100644 --- a/apps/language_server/lib/language_server/server.ex +++ b/apps/language_server/lib/language_server/server.ex @@ -731,7 +731,7 @@ defmodule ElixirLS.LanguageServer.Server do end defp handle_request(folding_range_req(_id, uri), state) do - case state.source_files[uri] do + case get_source_file(state, uri) do nil -> {:error, :server_error, "Missing source file", state} From 6a921d93f4ad016a5812bb37e8d31063de59831e Mon Sep 17 00:00:00 2001 From: William Lanchantin Date: Thu, 18 Feb 2021 14:29:14 -0500 Subject: [PATCH 39/61] combine Enum.maps; add comment --- .../lib/language_server/providers/folding_range/heredoc.ex | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/apps/language_server/lib/language_server/providers/folding_range/heredoc.ex b/apps/language_server/lib/language_server/providers/folding_range/heredoc.ex index b5beef442..f2b70a04a 100644 --- a/apps/language_server/lib/language_server/providers/folding_range/heredoc.ex +++ b/apps/language_server/lib/language_server/providers/folding_range/heredoc.ex @@ -40,8 +40,11 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRange.Heredoc do defp convert_heredoc_groups_to_ranges(heredoc_groups) do heredoc_groups - |> Enum.map(&FoldingRange.Helpers.first_and_last_of_list/1) - |> Enum.map(fn {last, first} -> classify_group(first, last) end) + |> Enum.map(fn group -> + # Each group comes out of group_heredoc_tokens/1 reversed + {last, first} = FoldingRange.Helpers.first_and_last_of_list(group) + classify_group(first, last) + end) end defp classify_group({:bin_heredoc, {start_line, _, _}, _}, {_, {end_line, _, _}, _}) do From a8c54a2cfb798f284c28fca7d475f63fc1832cf3 Mon Sep 17 00:00:00 2001 From: William Lanchantin Date: Thu, 18 Feb 2021 14:36:40 -0500 Subject: [PATCH 40/61] add `for` and `case`; add comments for clarity --- .../language_server/providers/folding_range/token_pairs.ex | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/language_server/lib/language_server/providers/folding_range/token_pairs.ex b/apps/language_server/lib/language_server/providers/folding_range/token_pairs.ex index 4d2d300a6..707a2081c 100644 --- a/apps/language_server/lib/language_server/providers/folding_range/token_pairs.ex +++ b/apps/language_server/lib/language_server/providers/folding_range/token_pairs.ex @@ -8,12 +8,16 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRange.TokenPairs do "(": [:")"], "[": [:"]"], "{": [:"}"], + # do blocks do: [:catch, :rescue, :after, :else, :end], catch: [:rescue, :after, :else, :end], rescue: [:after, :else, :end], after: [:else, :end], else: [:end], + # other special forms with: [:do], + for: [:do], + case: [:do], fn: [:end] } From 91252f0bcd53aa594b498c097a7a21dbd524eeb5 Mon Sep 17 00:00:00 2001 From: William Lanchantin Date: Thu, 18 Feb 2021 16:08:41 -0500 Subject: [PATCH 41/61] attempt to deal with utf8/16 length calculations --- .../providers/folding_range/line.ex | 15 +++++---------- .../lib/language_server/source_file.ex | 14 ++++++++++---- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/apps/language_server/lib/language_server/providers/folding_range/line.ex b/apps/language_server/lib/language_server/providers/folding_range/line.ex index 4f7a32277..e2b25976e 100644 --- a/apps/language_server/lib/language_server/providers/folding_range/line.ex +++ b/apps/language_server/lib/language_server/providers/folding_range/line.ex @@ -3,23 +3,18 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRange.Line do FoldingRange helpers for lines. """ + alias ElixirLS.LanguageServer.SourceFile + @type cell :: {non_neg_integer(), non_neg_integer() | nil} @type t :: {String.t(), cell(), String.t()} @spec format_string(String.t()) :: [cell()] def format_string(text) do text - |> text_to_lines() + |> SourceFile.lines() |> embellish_lines_with_metadata() end - @spec embellish_lines_with_metadata(String.t()) :: [String.t()] - defp text_to_lines(text) do - text - |> String.trim() - |> String.split(["\r\n", "\n", "\r"]) - end - # If we think of the code text as a grid, this function finds the cells whose # columns are the start of each row (line). # Empty rows are represented as {row, nil}. @@ -29,9 +24,9 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRange.Line do lines |> Enum.with_index() |> Enum.map(fn {line, row} -> - full_length = line |> String.length() + full_length = line |> SourceFile.line_length_utf16() trimmed = line |> String.trim_leading() - trimmed_length = trimmed |> String.length() + trimmed_length = trimmed |> SourceFile.line_length_utf16() first = trimmed |> String.first() col = diff --git a/apps/language_server/lib/language_server/source_file.ex b/apps/language_server/lib/language_server/source_file.ex index dc0df0d3b..3929a8425 100644 --- a/apps/language_server/lib/language_server/source_file.ex +++ b/apps/language_server/lib/language_server/source_file.ex @@ -159,12 +159,11 @@ defmodule ElixirLS.LanguageServer.SourceFile do def full_range(source_file) do lines = lines(source_file) - last_line = List.last(lines) utf16_size = - :unicode.characters_to_binary(last_line, :utf8, :utf16) - |> byte_size() - |> div(2) + lines + |> List.last() + |> line_length_utf16() %{ "start" => %{"line" => 0, "character" => 0}, @@ -172,6 +171,13 @@ defmodule ElixirLS.LanguageServer.SourceFile do } end + def line_length_utf16(line) do + line + |> :unicode.characters_to_binary(:utf8, :utf16) + |> byte_size() + |> div(2) + end + defp prepend_line(line, nil, acc), do: [line | acc] defp prepend_line(line, ending, acc), do: [[line, ending] | acc] From 195accacdac81bf5f9fe405b202fefb4af772714 Mon Sep 17 00:00:00 2001 From: William Lanchantin Date: Thu, 18 Feb 2021 16:36:39 -0500 Subject: [PATCH 42/61] fix misunderstanding and use :block_identifier --- .../providers/folding_range/token_pairs.ex | 7 +--- .../test/providers/folding_range_test.exs | 42 +++++++++++++++++++ 2 files changed, 44 insertions(+), 5 deletions(-) diff --git a/apps/language_server/lib/language_server/providers/folding_range/token_pairs.ex b/apps/language_server/lib/language_server/providers/folding_range/token_pairs.ex index 707a2081c..3bb26efd3 100644 --- a/apps/language_server/lib/language_server/providers/folding_range/token_pairs.ex +++ b/apps/language_server/lib/language_server/providers/folding_range/token_pairs.ex @@ -9,11 +9,8 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRange.TokenPairs do "[": [:"]"], "{": [:"}"], # do blocks - do: [:catch, :rescue, :after, :else, :end], - catch: [:rescue, :after, :else, :end], - rescue: [:after, :else, :end], - after: [:else, :end], - else: [:end], + do: [:block_identifier, :end], + block_identifier: [:block_identifier, :end], # other special forms with: [:do], for: [:do], diff --git a/apps/language_server/test/providers/folding_range_test.exs b/apps/language_server/test/providers/folding_range_test.exs index a4ccda953..fc13cab15 100644 --- a/apps/language_server/test/providers/folding_range_test.exs +++ b/apps/language_server/test/providers/folding_range_test.exs @@ -124,6 +124,48 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRangeTest do assert compare_condensed_ranges(ranges, [{0, 3}, {1, 2}]) end + @tag text: """ + defmodule A do # 0 + def hello() do # 1 + if true do # 2 + :hello # 3 + else # 4 + :error # 5 + end # 6 + end # 7 + end # 8 + """ + test "if-do-else-end", %{ranges_result: ranges_result} do + assert {:ok, ranges} = ranges_result + assert compare_condensed_ranges(ranges, [{0, 7}, {1, 6}, {2, 3}, {4, 5}]) + end + + @tag text: """ + defmodule A do # 0 + def hello() do # 1 + try do # 2 + :hello # 3 + rescue # 4 + ArgumentError -> # 5 + IO.puts("rescue") # 6 + catch # 7 + value -> # 8 + IO.puts("catch") # 9 + else # 10 + value -> # 11 + IO.puts("else") # 12 + after # 13 + IO.puts("after") # 14 + end # 15 + end # 16 + end # 17 + """ + test "try block", %{ranges_result: ranges_result} do + assert {:ok, ranges} = ranges_result + expected = [{0, 16}, {1, 15}, {2, 3}, {4, 6}, {7, 9}, {10, 12}, {13, 14}] + assert compare_condensed_ranges(ranges, expected) + end + @tag text: """ defmodule A do # 0 def hello() do # 1 From df73fab04dba1785ec2ca71b7bb720bfb4b9f4d1 Mon Sep 17 00:00:00 2001 From: billylanchantin Date: Fri, 19 Feb 2021 10:54:12 -0500 Subject: [PATCH 43/61] speling is hardd MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Étienne Lévesque --- .../lib/language_server/providers/folding_range/token_pairs.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/language_server/lib/language_server/providers/folding_range/token_pairs.ex b/apps/language_server/lib/language_server/providers/folding_range/token_pairs.ex index 3bb26efd3..d0e88e72b 100644 --- a/apps/language_server/lib/language_server/providers/folding_range/token_pairs.ex +++ b/apps/language_server/lib/language_server/providers/folding_range/token_pairs.ex @@ -47,7 +47,7 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRange.TokenPairs do # - The returned pairs will be ordered by the line of the 2nd element. # - Tokenizer.tokenize/1 doesn't differentiate between successful and failed # attempts to tokenize the string. - # This could mean the returned tokens are unbalaned. + # This could mean the returned tokens are unbalanced. # Therefore, the stack may not be empty when the base clause is hit. # We're choosing to return the successfully paired tokens rather than to # return an error if not all tokens could be paired. From b67a9f52a2334718ccac30dd1649508eff8eb61c Mon Sep 17 00:00:00 2001 From: William Lanchantin Date: Fri, 19 Feb 2021 11:18:21 -0500 Subject: [PATCH 44/61] make group_comments/1 a defp; add @specs --- .../providers/folding_range/comment_block.ex | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/apps/language_server/lib/language_server/providers/folding_range/comment_block.ex b/apps/language_server/lib/language_server/providers/folding_range/comment_block.ex index be00853f2..0917d82ff 100644 --- a/apps/language_server/lib/language_server/providers/folding_range/comment_block.ex +++ b/apps/language_server/lib/language_server/providers/folding_range/comment_block.ex @@ -23,12 +23,8 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRange.CommentBlock do {:ok, ranges} end - @doc """ - Pairs cells into {start, end} tuples of regions - Public function for testing - """ - @spec group_comments([Line.t()]) :: [any()] - def group_comments(lines) do + @spec group_comments([Line.t()]) :: [{Line.cell(), String.t()}] + defp group_comments(lines) do lines |> Enum.reduce([[]], fn {_, cell, "#"}, [[{_, "#"} | _] = head | tail] -> @@ -46,6 +42,7 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRange.CommentBlock do |> Enum.filter(fn group -> length(group) > 1 end) end + @spec group_comments([{Line.cell(), String.t()}]) :: [FoldingRange.t()] defp convert_comment_group_to_range(group) do {{{end_line, _}, _}, {{start_line, _}, _}} = group |> FoldingRange.Helpers.first_and_last_of_list() From fe82f5265451c7bfa5f2f642c82cc8f455608dca Mon Sep 17 00:00:00 2001 From: William Lanchantin Date: Fri, 19 Feb 2021 11:37:16 -0500 Subject: [PATCH 45/61] replace a nested-reduce with a flat_map + group_by --- .../lib/language_server/providers/folding_range.ex | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/apps/language_server/lib/language_server/providers/folding_range.ex b/apps/language_server/lib/language_server/providers/folding_range.ex index 70504d7b5..42b969c4c 100644 --- a/apps/language_server/lib/language_server/providers/folding_range.ex +++ b/apps/language_server/lib/language_server/providers/folding_range.ex @@ -78,13 +78,8 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRange do defp merge_ranges_with_priorities(range_lists_with_priorities) do range_lists_with_priorities - |> Enum.reduce(%{}, fn {priority, ranges}, acc -> - ranges - |> Enum.reduce(acc, fn %{startLine: start} = range, acc -> - ranges_with_priority = Map.get(acc, start, []) - Map.put(acc, start, [{priority, range} | ranges_with_priority]) - end) - end) + |> Enum.flat_map(fn {priority, ranges} -> Enum.zip(Stream.cycle([priority]), ranges) end) + |> Enum.group_by(fn {priority, range} -> range.startLine end) |> Enum.map(fn {_start, ranges_with_priority} -> {_priority, range} = ranges_with_priority From 2d2067154ffdc021fbf611bf7430ef96ef6875af Mon Sep 17 00:00:00 2001 From: William Lanchantin Date: Fri, 19 Feb 2021 11:57:13 -0500 Subject: [PATCH 46/61] drop kind_map, use @token_pairs directly --- .../providers/folding_range/token_pairs.ex | 23 +++++++++---------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/apps/language_server/lib/language_server/providers/folding_range/token_pairs.ex b/apps/language_server/lib/language_server/providers/folding_range/token_pairs.ex index d0e88e72b..3a225733a 100644 --- a/apps/language_server/lib/language_server/providers/folding_range/token_pairs.ex +++ b/apps/language_server/lib/language_server/providers/folding_range/token_pairs.ex @@ -26,15 +26,15 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRange.TokenPairs do def provide_ranges(%{tokens: tokens}) do ranges = tokens - |> pair_tokens(@token_pairs) + |> pair_tokens() |> convert_to_spec_ranges() {:ok, ranges} end - defp pair_tokens(tokens, kind_map) do + defp pair_tokens(tokens) do tokens - |> do_pair_tokens([], [], kind_map) + |> do_pair_tokens([], []) |> Enum.map(fn {{_, {start_line, _, _}, _}, {_, {end_line, _, _}, _}} -> # -1 for end_line because the range should stop 1 short # e.g. both "do" and "end" should be visible when collapsed @@ -51,22 +51,21 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRange.TokenPairs do # Therefore, the stack may not be empty when the base clause is hit. # We're choosing to return the successfully paired tokens rather than to # return an error if not all tokens could be paired. - defp do_pair_tokens([], _stack, pairs, _kind_map), do: pairs + defp do_pair_tokens([], _stack, pairs), do: pairs - defp do_pair_tokens([{head_kind, _, _} = head | tail_tokens], [], pairs, kind_map) do - new_stack = if kind_map |> Map.has_key?(head_kind), do: [head], else: [] - do_pair_tokens(tail_tokens, new_stack, pairs, kind_map) + defp do_pair_tokens([{head_kind, _, _} = head | tail_tokens], [], pairs) do + new_stack = if @token_pairs |> Map.has_key?(head_kind), do: [head], else: [] + do_pair_tokens(tail_tokens, new_stack, pairs) end defp do_pair_tokens( [{head_kind, _, _} = head | tail_tokens], [{top_kind, _, _} = top | tail_stack] = stack, - pairs, - kind_map + pairs ) do - head_matches_any? = kind_map |> Map.has_key?(head_kind) + head_matches_any? = @token_pairs |> Map.has_key?(head_kind) # Map.get/2 will always succeed because we only push matches to the stack. - head_matches_top? = kind_map |> Map.get(top_kind) |> Enum.member?(head_kind) + head_matches_top? = @token_pairs |> Map.get(top_kind) |> Enum.member?(head_kind) {new_stack, new_pairs} = case {head_matches_any?, head_matches_top?} do @@ -76,7 +75,7 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRange.TokenPairs do {true, true} -> {[head | tail_stack], [{top, head} | pairs]} end - do_pair_tokens(tail_tokens, new_stack, new_pairs, kind_map) + do_pair_tokens(tail_tokens, new_stack, new_pairs) end defp convert_to_spec_ranges(ranges) do From 5c291d37d48adfc85622fea37f93f487c77dd73c Mon Sep 17 00:00:00 2001 From: William Lanchantin Date: Fri, 19 Feb 2021 12:07:02 -0500 Subject: [PATCH 47/61] refactor, change name, add @specs --- .../providers/folding_range/token_pairs.ex | 30 ++++++++----------- 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/apps/language_server/lib/language_server/providers/folding_range/token_pairs.ex b/apps/language_server/lib/language_server/providers/folding_range/token_pairs.ex index 3a225733a..ff8cb3f76 100644 --- a/apps/language_server/lib/language_server/providers/folding_range/token_pairs.ex +++ b/apps/language_server/lib/language_server/providers/folding_range/token_pairs.ex @@ -3,6 +3,7 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRange.TokenPairs do """ alias ElixirLS.LanguageServer.Providers.FoldingRange + alias ElixirLS.LanguageServer.Providers.FoldingRange.Token @token_pairs %{ "(": [:")"], @@ -27,19 +28,14 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRange.TokenPairs do ranges = tokens |> pair_tokens() - |> convert_to_spec_ranges() + |> convert_token_pairs_to_ranges() {:ok, ranges} end + @spec pair_tokens([Token.t()]) :: [{Token.t(), Token.t()}] defp pair_tokens(tokens) do - tokens - |> do_pair_tokens([], []) - |> Enum.map(fn {{_, {start_line, _, _}, _}, {_, {end_line, _, _}, _}} -> - # -1 for end_line because the range should stop 1 short - # e.g. both "do" and "end" should be visible when collapsed - {start_line, end_line - 1} - end) + do_pair_tokens(tokens, [], []) end # A stack-based approach to match range pairs @@ -78,17 +74,15 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRange.TokenPairs do do_pair_tokens(tail_tokens, new_stack, new_pairs) end - defp convert_to_spec_ranges(ranges) do - ranges + @spec convert_token_pairs_to_ranges([{Token.t(), Token.t()}]) :: [FoldingRange.t()] + defp convert_token_pairs_to_ranges(token_pairs) do + token_pairs + |> Enum.map(fn {{_, {start_line, _, _}, _}, {_, {end_line, _, _}, _}} -> + # -1 for end_line because the range should stop 1 short + # e.g. both "do" and "end" should be visible when collapsed + {start_line, end_line - 1} + end) |> Enum.filter(fn {start_line, end_line} -> end_line > start_line end) - |> Enum.sort() - |> Enum.dedup() - ## Remove the above sort + dedup lines and uncomment the following if no - ## two ranges may share a startLine - # |> Enum.group_by(fn {start_line, _} -> start_line end) - # |> Enum.map(fn {_, ranges} -> - # Enum.max_by(ranges, fn {_, end_line} -> end_line end) - # end) |> Enum.map(fn {start_line, end_line} -> %{startLine: start_line, endLine: end_line, kind?: :region} end) From 888814a0fefd7b82e83b89008d267077d8096d6a Mon Sep 17 00:00:00 2001 From: William Lanchantin Date: Fri, 19 Feb 2021 12:14:06 -0500 Subject: [PATCH 48/61] use pipes --- .../providers/folding_range/helpers.ex | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/apps/language_server/lib/language_server/providers/folding_range/helpers.ex b/apps/language_server/lib/language_server/providers/folding_range/helpers.ex index c3ef803c1..019b190d2 100644 --- a/apps/language_server/lib/language_server/providers/folding_range/helpers.ex +++ b/apps/language_server/lib/language_server/providers/folding_range/helpers.ex @@ -1,14 +1,14 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRange.Helpers do - @moduledoc """ - """ + @moduledoc false + def first_and_last_of_list([]), do: :empty_list - def first_and_last_of_list(list) when is_list(list) do - [head | tail] = list - do_falo_list(tail, head) + def first_and_last_of_list([head | tail]) do + tail + |> List.last() + |> case do + nil -> {head, head} + last -> {head, last} + end end - - defp do_falo_list([], first), do: {first, first} - defp do_falo_list([last | []], first), do: {first, last} - defp do_falo_list([_ | tail], first), do: do_falo_list(tail, first) end From 6bb06f873a54b2eddfe780d12c527577ba6ea606 Mon Sep 17 00:00:00 2001 From: William Lanchantin Date: Fri, 19 Feb 2021 12:24:05 -0500 Subject: [PATCH 49/61] fix warning --- .../lib/language_server/providers/folding_range.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/language_server/lib/language_server/providers/folding_range.ex b/apps/language_server/lib/language_server/providers/folding_range.ex index 42b969c4c..08b7b6905 100644 --- a/apps/language_server/lib/language_server/providers/folding_range.ex +++ b/apps/language_server/lib/language_server/providers/folding_range.ex @@ -79,7 +79,7 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRange do defp merge_ranges_with_priorities(range_lists_with_priorities) do range_lists_with_priorities |> Enum.flat_map(fn {priority, ranges} -> Enum.zip(Stream.cycle([priority]), ranges) end) - |> Enum.group_by(fn {priority, range} -> range.startLine end) + |> Enum.group_by(fn {_priority, range} -> range.startLine end) |> Enum.map(fn {_start, ranges_with_priority} -> {_priority, range} = ranges_with_priority From 254b09301fd936b5a91a74db7befa04acb41b723 Mon Sep 17 00:00:00 2001 From: William Lanchantin Date: Sat, 20 Feb 2021 10:54:48 -0500 Subject: [PATCH 50/61] add support for charlist heredocs --- .../providers/folding_range/heredoc.ex | 15 +++++++-------- .../test/providers/folding_range_test.exs | 14 ++++++++++++++ 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/apps/language_server/lib/language_server/providers/folding_range/heredoc.ex b/apps/language_server/lib/language_server/providers/folding_range/heredoc.ex index f2b70a04a..5f6ff72f3 100644 --- a/apps/language_server/lib/language_server/providers/folding_range/heredoc.ex +++ b/apps/language_server/lib/language_server/providers/folding_range/heredoc.ex @@ -4,6 +4,8 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRange.Heredoc do alias ElixirLS.LanguageServer.Providers.FoldingRange + @heredoc_kinds [:bin_heredoc, :list_heredoc] + @spec provide_ranges([FoldingRange.input()]) :: {:ok, [FoldingRange.t()]} def provide_ranges(%{tokens: tokens}) do ranges = @@ -14,23 +16,19 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRange.Heredoc do {:ok, ranges} end - # The :bin_heredoc token will be either - # - by itself or - # - directly after an :identifier (either :doc or :moduledoc). - # :bin_heredoc regions are ended by an :eol token. defp group_heredoc_tokens(tokens) do tokens |> Enum.reduce([], fn {:identifier, _, x} = token, acc when x in [:doc, :moduledoc] -> [[token] | acc] - {:bin_heredoc, _, _} = token, [[{:identifier, _, _}] = head | tail] -> + {kind, _, _} = token, [[{:identifier, _, _}] = head | tail] when kind in @heredoc_kinds -> [[token | head] | tail] - {:bin_heredoc, _, _} = token, acc -> + {kind, _, _} = token, acc when kind in @heredoc_kinds -> [[token] | acc] - {:eol, _, _} = token, [[{:bin_heredoc, _, _} | _] = head | tail] -> + {:eol, _, _} = token, [[{kind, _, _} | _] = head | tail] when kind in @heredoc_kinds -> [[token | head] | tail] _, acc -> @@ -47,7 +45,8 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRange.Heredoc do end) end - defp classify_group({:bin_heredoc, {start_line, _, _}, _}, {_, {end_line, _, _}, _}) do + defp classify_group({kind, {start_line, _, _}, _}, {_, {end_line, _, _}, _}) + when kind in @heredoc_kinds do %{startLine: start_line, endLine: end_line - 1, kind?: :region} end diff --git a/apps/language_server/test/providers/folding_range_test.exs b/apps/language_server/test/providers/folding_range_test.exs index fc13cab15..d9f977f0e 100644 --- a/apps/language_server/test/providers/folding_range_test.exs +++ b/apps/language_server/test/providers/folding_range_test.exs @@ -292,6 +292,20 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRangeTest do assert compare_condensed_ranges(ranges, [{1, 2}, {5, 6}, {19, 20}]) end + @tag text: """ + defmodule A do # 0 + def hello() do # 1 + ''' + charlist heredoc # 3 + ''' + end # 5 + end # 6 + """ + test "charlist heredocs", %{ranges_result: ranges_result} do + assert {:ok, ranges} = ranges_result + assert compare_condensed_ranges(ranges, [{2, 3}]) + end + defp fold_via_heredocs(%{text: text} = context) do ranges_result = text From 5b9be3b8397cf9988b3fd6d239a2491d857d28ed Mon Sep 17 00:00:00 2001 From: William Lanchantin Date: Sat, 20 Feb 2021 12:02:20 -0500 Subject: [PATCH 51/61] add binary support --- .../providers/folding_range/token_pairs.ex | 1 + .../test/providers/folding_range_test.exs | 16 ++++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/apps/language_server/lib/language_server/providers/folding_range/token_pairs.ex b/apps/language_server/lib/language_server/providers/folding_range/token_pairs.ex index ff8cb3f76..58b7c633b 100644 --- a/apps/language_server/lib/language_server/providers/folding_range/token_pairs.ex +++ b/apps/language_server/lib/language_server/providers/folding_range/token_pairs.ex @@ -9,6 +9,7 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRange.TokenPairs do "(": [:")"], "[": [:"]"], "{": [:"}"], + "<<": [:">>"], # do blocks do: [:block_identifier, :end], block_identifier: [:block_identifier, :end], diff --git a/apps/language_server/test/providers/folding_range_test.exs b/apps/language_server/test/providers/folding_range_test.exs index d9f977f0e..8a0c18f9d 100644 --- a/apps/language_server/test/providers/folding_range_test.exs +++ b/apps/language_server/test/providers/folding_range_test.exs @@ -186,6 +186,22 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRangeTest do assert compare_condensed_ranges(ranges, [{0, 11}, {1, 10}, {4, 9}]) end + @tag text: """ + defmodule A do # 0 + def hello() do # 1 + <<0>> # 2 + << # 3 + 1, 2, 3, # 4 + 4, 5, 6 # 5 + >> # 6 + end # 7 + end # 8 + """ + test "binaries", %{ranges_result: ranges_result} do + assert {:ok, ranges} = ranges_result + assert compare_condensed_ranges(ranges, [{0, 7}, {1, 6}, {3, 5}]) + end + @tag text: """ defmodule A do # 0 @moduledoc "This is module A" # 1 From dc1a27a844c8ac2faecb51fc75c5b4896c1f200c Mon Sep 17 00:00:00 2001 From: William Lanchantin Date: Sat, 20 Feb 2021 14:42:33 -0500 Subject: [PATCH 52/61] tweak test approach to allow specifying range kind --- .../test/providers/folding_range_test.exs | 38 +++++++++++++++++-- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/apps/language_server/test/providers/folding_range_test.exs b/apps/language_server/test/providers/folding_range_test.exs index 8a0c18f9d..9f426667d 100644 --- a/apps/language_server/test/providers/folding_range_test.exs +++ b/apps/language_server/test/providers/folding_range_test.exs @@ -431,8 +431,40 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRangeTest do end end - defp compare_condensed_ranges(result, condensed_expected) do - condensed_result = result |> Enum.map(&{&1.startLine, &1.endLine}) - assert Enum.sort(condensed_result) == Enum.sort(condensed_expected) + defp compare_condensed_ranges(result, expected_condensed) do + result_condensed = + result + |> Enum.map(fn + %{startLine: start_line, endLine: end_line, kind?: kind} -> + {start_line, end_line, kind} + + %{startLine: start_line, endLine: end_line} -> + {start_line, end_line, :any} + end) + |> Enum.sort() + + expected_condensed = + expected_condensed + |> Enum.map(fn + {start_line, end_line, kind} -> + {start_line, end_line, kind} + + {start_line, end_line} -> + {start_line, end_line, :any} + end) + |> Enum.sort() + + {result_condensed, expected_condensed} = + Enum.zip(result_condensed, expected_condensed) + |> Enum.map(fn + {{rs, re, rk}, {es, ee, ek}} when rk == :any or ek == :any -> + {{rs, re, :any}, {es, ee, :any}} + + otherwise -> + otherwise + end) + |> Enum.unzip() + + assert result_condensed == expected_condensed end end From 21f1a7b3924043867339f73d0c2e909a543d24fc Mon Sep 17 00:00:00 2001 From: William Lanchantin Date: Sat, 20 Feb 2021 14:43:24 -0500 Subject: [PATCH 53/61] change heredoc pass to "special token" pass; add/modify tests --- .../providers/folding_range.ex | 6 +- .../providers/folding_range/heredoc.ex | 52 +++++--- .../providers/folding_range/token.ex | 35 +++++- .../test/providers/folding_range_test.exs | 116 +++++++++++++----- 4 files changed, 152 insertions(+), 57 deletions(-) diff --git a/apps/language_server/lib/language_server/providers/folding_range.ex b/apps/language_server/lib/language_server/providers/folding_range.ex index 08b7b6905..4651d6d9d 100644 --- a/apps/language_server/lib/language_server/providers/folding_range.ex +++ b/apps/language_server/lib/language_server/providers/folding_range.ex @@ -53,17 +53,17 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRange do defp do_provide(text) do input = convert_text_to_input(text) - {:ok, token_pair_ranges} = input |> FoldingRange.TokenPairs.provide_ranges() {:ok, indentation_ranges} = input |> FoldingRange.Indentation.provide_ranges() - {:ok, heredoc_ranges} = input |> FoldingRange.Heredoc.provide_ranges() {:ok, comment_block_ranges} = input |> FoldingRange.CommentBlock.provide_ranges() + {:ok, token_pair_ranges} = input |> FoldingRange.TokenPairs.provide_ranges() + {:ok, special_token_ranges} = input |> FoldingRange.SpecialToken.provide_ranges() ranges = merge_ranges_with_priorities([ {1, indentation_ranges}, {2, comment_block_ranges}, {3, token_pair_ranges}, - {3, heredoc_ranges} + {3, special_token_ranges} ]) {:ok, ranges} diff --git a/apps/language_server/lib/language_server/providers/folding_range/heredoc.ex b/apps/language_server/lib/language_server/providers/folding_range/heredoc.ex index 5f6ff72f3..3b59a8a55 100644 --- a/apps/language_server/lib/language_server/providers/folding_range/heredoc.ex +++ b/apps/language_server/lib/language_server/providers/folding_range/heredoc.ex @@ -1,34 +1,43 @@ -defmodule ElixirLS.LanguageServer.Providers.FoldingRange.Heredoc do +defmodule ElixirLS.LanguageServer.Providers.FoldingRange.SpecialToken do @moduledoc """ + TODO: document this """ alias ElixirLS.LanguageServer.Providers.FoldingRange + alias ElixirLS.LanguageServer.Providers.FoldingRange.Token - @heredoc_kinds [:bin_heredoc, :list_heredoc] + @kinds [ + :bin_heredoc, + :bin_string, + :list_heredoc, + :list_string, + :sigil + ] @spec provide_ranges([FoldingRange.input()]) :: {:ok, [FoldingRange.t()]} def provide_ranges(%{tokens: tokens}) do ranges = tokens - |> group_heredoc_tokens() - |> convert_heredoc_groups_to_ranges() + |> group_tokens() + |> convert_groups_to_ranges() {:ok, ranges} end - defp group_heredoc_tokens(tokens) do + @spec group_tokens([Token.t()]) :: [[Token.t()]] + defp group_tokens(tokens) do tokens |> Enum.reduce([], fn - {:identifier, _, x} = token, acc when x in [:doc, :moduledoc] -> + {:identifier, _, identifier} = token, acc when identifier in [:doc, :moduledoc] -> [[token] | acc] - {kind, _, _} = token, [[{:identifier, _, _}] = head | tail] when kind in @heredoc_kinds -> + {k, _, _} = token, [[{:identifier, _, _}] = head | tail] when k in @kinds -> [[token | head] | tail] - {kind, _, _} = token, acc when kind in @heredoc_kinds -> + {k, _, _} = token, acc when k in @kinds -> [[token] | acc] - {:eol, _, _} = token, [[{kind, _, _} | _] = head | tail] when kind in @heredoc_kinds -> + {:eol, _, _} = token, [[{k, _, _} | _] = head | tail] when k in @kinds -> [[token | head] | tail] _, acc -> @@ -36,21 +45,26 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRange.Heredoc do end) end - defp convert_heredoc_groups_to_ranges(heredoc_groups) do - heredoc_groups + @spec convert_groups_to_ranges([[Token.t()]]) :: [FoldingRange.t()] + defp convert_groups_to_ranges(groups) do + groups |> Enum.map(fn group -> - # Each group comes out of group_heredoc_tokens/1 reversed + # Each group comes out of group_tokens/1 reversed {last, first} = FoldingRange.Helpers.first_and_last_of_list(group) classify_group(first, last) end) + |> Enum.map(fn {start_line, end_line, kind} -> + %{ + startLine: start_line, + endLine: end_line - 1, + kind?: kind + } + end) + |> Enum.filter(fn range -> range.endLine > range.startLine end) end - defp classify_group({kind, {start_line, _, _}, _}, {_, {end_line, _, _}, _}) - when kind in @heredoc_kinds do - %{startLine: start_line, endLine: end_line - 1, kind?: :region} - end - - defp classify_group({:identifier, {start_line, _, _}, _}, {_, {end_line, _, _}, _}) do - %{startLine: start_line, endLine: end_line - 1, kind?: :comment} + defp classify_group({kind, {start_line, _, _}, _}, {_, {end_line, _, _}, _}) do + kind = if kind == :identifier, do: :comment, else: :region + {start_line, end_line, kind} end end diff --git a/apps/language_server/lib/language_server/providers/folding_range/token.ex b/apps/language_server/lib/language_server/providers/folding_range/token.ex index 2b3a15ff4..7872c4571 100644 --- a/apps/language_server/lib/language_server/providers/folding_range/token.ex +++ b/apps/language_server/lib/language_server/providers/folding_range/token.ex @@ -20,10 +20,18 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRange.Token do |> Enum.reduce_while({:ok, []}, fn tuple, {:ok, acc} -> tuple = case tuple do - {a, {b1, b2, b3}} -> {a, {b1 - 1, b2 - 1, b3}, nil} - {a, {b1, b2, b3}, c} -> {a, {b1 - 1, b2 - 1, b3}, c} + {a, {b1, b2, b3}} -> + {a, {b1 - 1, b2 - 1, b3}, nil} + + {a, {b1, b2, b3}, c} -> + {a, {b1 - 1, b2 - 1, b3}, c} + + {:sigil, {b1, b2, b3}, _, _, _, _, delimiter} -> + {:sigil, {b1 - 1, b2 - 1, b3}, delimiter} + # raise here? - _ -> :error + _ -> + :error end if tuple == :error do @@ -37,4 +45,25 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRange.Token do _ -> [] end end + + @doc """ + This reproduces the internals of Token.tokenize/1. + It's helpful for debuging because it doesn't hide what went wrong. + """ + def tokenize_debug(prefix) do + prefix + |> String.to_charlist() + |> do_tokenize_1_7() + end + + defp do_tokenize_1_7(prefix_charlist) do + case :elixir_tokenizer.tokenize(prefix_charlist, 1, []) do + {:ok, tokens} -> + {:ok, tokens} + + # write it like this so I know what the parts are + {:error, {line, column, error_prefix, token}, rest, sofar} -> + {:error, {line, column, error_prefix, token}, rest, sofar} + end + end end diff --git a/apps/language_server/test/providers/folding_range_test.exs b/apps/language_server/test/providers/folding_range_test.exs index 9f426667d..6473851c1 100644 --- a/apps/language_server/test/providers/folding_range_test.exs +++ b/apps/language_server/test/providers/folding_range_test.exs @@ -271,62 +271,114 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRangeTest do end end - describe "heredocs" do - setup [:fold_via_heredocs] + describe "special tokens" do + setup [:fold_via_special_tokens] @tag text: """ - defmodule A do # 0 + defmodule A do # 0 @moduledoc \"\"\" - I'm a @moduledoc heredoc. # 2 + @moduledoc heredoc # 2 \"\"\" @doc \"\"\" - I'm a @doc heredoc. # 6 + @doc heredoc # 6 \"\"\" - def f(%{"key" => value} = map) do # 8 - case NaiveDateTime.from_iso8601(value) do # 9 - {:ok, ndt} -> # 10 - dt = # 11 - ndt # 12 - |> DateTime.from_naive!("Etc/UTC") # 13 - |> Map.put(:microsecond, {0, 6}) # 14 - - %{map | "key" => dt} # 16 - - e -> # 18 - Logger.warn(\"\"\" - I'm a regular heredoc # 20 - \"\"\") - - :could_not_parse_value # 23 - end # 24 - end # 25 - end # 26 + def hello() do # 8 + \"\"\" + regular heredoc # 10 + \"\"\" + end # 12 + end # 13 """ test "@moduledoc, @doc, and stand-alone heredocs", %{ranges_result: ranges_result} do assert {:ok, ranges} = ranges_result - assert compare_condensed_ranges(ranges, [{1, 2}, {5, 6}, {19, 20}]) + expected = [{1, 2, :comment}, {5, 6, :comment}, {9, 10, :region}] + assert compare_condensed_ranges(ranges, expected) end @tag text: """ defmodule A do # 0 def hello() do # 1 + " + regular string # 3 + " + ' + charlist string # 6 + ' + \"\"\" + regular heredoc # 9 + \"\"\" ''' - charlist heredoc # 3 + charlist heredoc # 12 ''' - end # 5 - end # 6 + end # 14 + end # 15 """ test "charlist heredocs", %{ranges_result: ranges_result} do assert {:ok, ranges} = ranges_result - assert compare_condensed_ranges(ranges, [{2, 3}]) + assert compare_condensed_ranges(ranges, [{2, 3}, {5, 6}, {8, 9}, {11, 12}]) + end + + @tag text: """ + defmodule A do # 0 + def hello() do # 1 + ~r/ + hello # 3 + / + ~r| + hello # 6 + | + ~r" + hello # 9 + " + ~r' + hello # 12 + ' + ~r( + hello # 15 + ) + ~r[ + hello # 18 + ] + ~r{ + hello # 21 + } + ~r< + hello # 24 + > + end # 26 + end # 27 + """ + test "sigil delimiters", %{ranges_result: ranges_result} do + assert {:ok, ranges} = ranges_result + expected = [{2, 3}, {5, 6}, {8, 9}, {11, 12}, {14, 15}, {17, 18}, {20, 21}, {23, 24}] + assert compare_condensed_ranges(ranges, expected) + end + + @tag text: """ + defmodule A do # 0 + @module doc ~S\"\"\" + sigil @moduledoc # 2 + \"\"\" + + @doc ~S\"\"\" + sigil @doc # 6 + \"\"\" + def hello() do # 8 + :world # 9 + end # 10 + end # 11 + """ + test "@doc with ~S sigil", %{ranges_result: ranges_result} do + assert {:ok, ranges} = ranges_result + assert compare_condensed_ranges(ranges, [{1, 2, :comment}, {5, 6, :comment}]) end - defp fold_via_heredocs(%{text: text} = context) do + defp fold_via_special_tokens(%{text: text} = context) do ranges_result = text |> FoldingRange.convert_text_to_input() - |> FoldingRange.Heredoc.provide_ranges() + |> FoldingRange.SpecialToken.provide_ranges() {:ok, Map.put(context, :ranges_result, ranges_result)} end @@ -380,7 +432,7 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRangeTest do @tag text: """ defmodule A do # 0 - @moduledoc \"\"\" + @moduledoc ~S\"\"\" I'm a @moduledoc heredoc. # 2 \"\"\" From af1f545b95db0538eee8d0ec746fa2d20cc1617c Mon Sep 17 00:00:00 2001 From: William Lanchantin Date: Sat, 20 Feb 2021 14:43:45 -0500 Subject: [PATCH 54/61] change filename --- .../providers/folding_range/{heredoc.ex => special_token.ex} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename apps/language_server/lib/language_server/providers/folding_range/{heredoc.ex => special_token.ex} (100%) diff --git a/apps/language_server/lib/language_server/providers/folding_range/heredoc.ex b/apps/language_server/lib/language_server/providers/folding_range/special_token.ex similarity index 100% rename from apps/language_server/lib/language_server/providers/folding_range/heredoc.ex rename to apps/language_server/lib/language_server/providers/folding_range/special_token.ex From 0b7296039a116035d695b9ba267a64113eb73332 Mon Sep 17 00:00:00 2001 From: William Lanchantin Date: Sat, 20 Feb 2021 14:44:58 -0500 Subject: [PATCH 55/61] change to singular module name for consistency --- .../lib/language_server/providers/folding_range.ex | 2 +- .../lib/language_server/providers/folding_range/token_pairs.ex | 2 +- apps/language_server/test/providers/folding_range_test.exs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/language_server/lib/language_server/providers/folding_range.ex b/apps/language_server/lib/language_server/providers/folding_range.ex index 4651d6d9d..8a26b8b3d 100644 --- a/apps/language_server/lib/language_server/providers/folding_range.ex +++ b/apps/language_server/lib/language_server/providers/folding_range.ex @@ -55,7 +55,7 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRange do input = convert_text_to_input(text) {:ok, indentation_ranges} = input |> FoldingRange.Indentation.provide_ranges() {:ok, comment_block_ranges} = input |> FoldingRange.CommentBlock.provide_ranges() - {:ok, token_pair_ranges} = input |> FoldingRange.TokenPairs.provide_ranges() + {:ok, token_pair_ranges} = input |> FoldingRange.TokenPair.provide_ranges() {:ok, special_token_ranges} = input |> FoldingRange.SpecialToken.provide_ranges() ranges = diff --git a/apps/language_server/lib/language_server/providers/folding_range/token_pairs.ex b/apps/language_server/lib/language_server/providers/folding_range/token_pairs.ex index 58b7c633b..55fa4129b 100644 --- a/apps/language_server/lib/language_server/providers/folding_range/token_pairs.ex +++ b/apps/language_server/lib/language_server/providers/folding_range/token_pairs.ex @@ -1,4 +1,4 @@ -defmodule ElixirLS.LanguageServer.Providers.FoldingRange.TokenPairs do +defmodule ElixirLS.LanguageServer.Providers.FoldingRange.TokenPair do @moduledoc """ """ diff --git a/apps/language_server/test/providers/folding_range_test.exs b/apps/language_server/test/providers/folding_range_test.exs index 6473851c1..1e1676c16 100644 --- a/apps/language_server/test/providers/folding_range_test.exs +++ b/apps/language_server/test/providers/folding_range_test.exs @@ -265,7 +265,7 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRangeTest do ranges_result = text |> FoldingRange.convert_text_to_input() - |> FoldingRange.TokenPairs.provide_ranges() + |> FoldingRange.TokenPair.provide_ranges() {:ok, Map.put(context, :ranges_result, ranges_result)} end From 29920e123a71d101bc9ca2a47e2b310d2b58efcb Mon Sep 17 00:00:00 2001 From: William Lanchantin Date: Sat, 20 Feb 2021 14:46:17 -0500 Subject: [PATCH 56/61] remove outdated comments --- .../providers/folding_range/token_pairs.ex | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/apps/language_server/lib/language_server/providers/folding_range/token_pairs.ex b/apps/language_server/lib/language_server/providers/folding_range/token_pairs.ex index 55fa4129b..e33e3d2e2 100644 --- a/apps/language_server/lib/language_server/providers/folding_range/token_pairs.ex +++ b/apps/language_server/lib/language_server/providers/folding_range/token_pairs.ex @@ -20,10 +20,6 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRange.TokenPair do fn: [:end] } - # Note - # This implementation allows for the possibility of 2 ranges with the same - # startLines but different endLines. - # It's not clear if that case is actually a problem. @spec provide_ranges([FoldingRange.input()]) :: {:ok, [FoldingRange.t()]} def provide_ranges(%{tokens: tokens}) do ranges = @@ -39,10 +35,8 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRange.TokenPair do do_pair_tokens(tokens, [], []) end - # A stack-based approach to match range pairs - # Notes - # - The returned pairs will be ordered by the line of the 2nd element. - # - Tokenizer.tokenize/1 doesn't differentiate between successful and failed + # Note + # Tokenizer.tokenize/1 doesn't differentiate between successful and failed # attempts to tokenize the string. # This could mean the returned tokens are unbalanced. # Therefore, the stack may not be empty when the base clause is hit. From f8bf33295942ba13590e93a1ad925db658a36d9b Mon Sep 17 00:00:00 2001 From: William Lanchantin Date: Sat, 20 Feb 2021 17:51:59 -0500 Subject: [PATCH 57/61] remove debug function --- .../providers/folding_range/token.ex | 21 ------------------- 1 file changed, 21 deletions(-) diff --git a/apps/language_server/lib/language_server/providers/folding_range/token.ex b/apps/language_server/lib/language_server/providers/folding_range/token.ex index 7872c4571..ea755e1db 100644 --- a/apps/language_server/lib/language_server/providers/folding_range/token.ex +++ b/apps/language_server/lib/language_server/providers/folding_range/token.ex @@ -45,25 +45,4 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRange.Token do _ -> [] end end - - @doc """ - This reproduces the internals of Token.tokenize/1. - It's helpful for debuging because it doesn't hide what went wrong. - """ - def tokenize_debug(prefix) do - prefix - |> String.to_charlist() - |> do_tokenize_1_7() - end - - defp do_tokenize_1_7(prefix_charlist) do - case :elixir_tokenizer.tokenize(prefix_charlist, 1, []) do - {:ok, tokens} -> - {:ok, tokens} - - # write it like this so I know what the parts are - {:error, {line, column, error_prefix, token}, rest, sofar} -> - {:error, {line, column, error_prefix, token}, rest, sofar} - end - end end From cc2fa59aca6ede37e763e9f120c987f7bf38c660 Mon Sep 17 00:00:00 2001 From: William Lanchantin Date: Mon, 22 Feb 2021 10:08:38 -0500 Subject: [PATCH 58/61] (hopefully) cover older versions of tokenize --- .../lib/language_server/providers/folding_range/token.ex | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/language_server/lib/language_server/providers/folding_range/token.ex b/apps/language_server/lib/language_server/providers/folding_range/token.ex index ea755e1db..083fd00ef 100644 --- a/apps/language_server/lib/language_server/providers/folding_range/token.ex +++ b/apps/language_server/lib/language_server/providers/folding_range/token.ex @@ -29,6 +29,10 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRange.Token do {:sigil, {b1, b2, b3}, _, _, _, _, delimiter} -> {:sigil, {b1 - 1, b2 - 1, b3}, delimiter} + # Older versions of Tokenizer.tokenize/1 + {:sigil, {b1, b2, b3}, _, _, _, delimiter} -> + {:sigil, {b1 - 1, b2 - 1, b3}, delimiter} + # raise here? _ -> :error From c3a3da4b686c31d9983f95f8c6c3d5e283e92007 Mon Sep 17 00:00:00 2001 From: William Lanchantin Date: Tue, 23 Feb 2021 09:35:29 -0500 Subject: [PATCH 59/61] documentation; harmless refactor --- .../providers/folding_range.ex | 108 +++++++++++++----- .../providers/folding_range/comment_block.ex | 32 +++++- .../providers/folding_range/indentation.ex | 40 ++++++- .../providers/folding_range/special_token.ex | 35 +++++- .../providers/folding_range/token.ex | 3 + .../providers/folding_range/token_pairs.ex | 31 +++++ 6 files changed, 215 insertions(+), 34 deletions(-) diff --git a/apps/language_server/lib/language_server/providers/folding_range.ex b/apps/language_server/lib/language_server/providers/folding_range.ex index 8a26b8b3d..a68e04ff2 100644 --- a/apps/language_server/lib/language_server/providers/folding_range.ex +++ b/apps/language_server/lib/language_server/providers/folding_range.ex @@ -2,8 +2,51 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRange do @moduledoc """ A textDocument/foldingRange provider implementation. + ## Background + See specification here: - https://microsoft.github.io/language-server-protocol/specifications/specification-3-15/#textDocument_foldingRange + + https://microsoft.github.io/language-server-protocol/specifications/specification-3-15/#textDocument_foldingRange + + ## Methodology + + ### High level + + We make multiple passes (currently 4) through the source text and create + folding ranges from each pass. + Then we merge the ranges from each pass to provide the final ranges. + Each pass gets a priority to help break ties (the priority is an integer, + higher integers win). + + ### Indentation pass (priority: 1) + + We use the indentation level -- determined by the column of the first + non-whitespace character on each line -- to provide baseline ranges. + All ranges from this pass are `kind?: :region` ranges. + + ### Comment block pass (priority: 2) + + We let "comment blocks", consecutive lines starting with `#`, from regions. + All ranges from this pass are `kind?: :comment` ranges. + + ### Token-pairs pass (priority: 3) + + We use pairs of tokens, e.g. `do` and `end`, to provide another pass of + ranges. + All ranges from this pass are `kind?: :region` ranges. + + ### Special tokens pass (priority: 3) + + We find strings (regular/charlist strings/heredocs) and sigils in a pass as + they're delimited by a few special tokens. + Ranges from this pass are either + - `kind?: :comment` if the token is paired with `@doc` or `@moduledoc`, or + - `kind?: :region` otherwise. + + ## Notes + + Each pass may return ranges in any order. + But all ranges are valid, i.e. endLine > startLine. """ alias __MODULE__ @@ -26,21 +69,23 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRange do ## Example - text = \"\"\" - defmodule A do # 0 - def hello() do # 1 - :world # 2 - end # 3 - end # 4 - \"\"\" - - {:ok, ranges} = FoldingRange.provide(%{text: text}) - - ranges - # [ - # %{startLine: 0, endLine: 3}, - # %{startLine: 1, endLine: 2} - # ] + text = \"\"\" + defmodule A do # 0 + def hello() do # 1 + :world # 2 + end # 3 + end # 4 + \"\"\" + + {:ok, ranges} = + text + |> convert_text_to_input() + |> provide() + + # ranges == [ + # %{startLine: 0, endLine: 3, kind?: :region}, + # %{startLine: 1, endLine: 2, kind?: :region} + # ] """ @spec provide(%{text: String.t()}) :: {:ok, [t()]} | {:error, String.t()} def provide(%{text: text}) do @@ -53,18 +98,21 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRange do defp do_provide(text) do input = convert_text_to_input(text) - {:ok, indentation_ranges} = input |> FoldingRange.Indentation.provide_ranges() - {:ok, comment_block_ranges} = input |> FoldingRange.CommentBlock.provide_ranges() - {:ok, token_pair_ranges} = input |> FoldingRange.TokenPair.provide_ranges() - {:ok, special_token_ranges} = input |> FoldingRange.SpecialToken.provide_ranges() + + passes_with_priority = [ + {1, FoldingRange.Indentation}, + {2, FoldingRange.CommentBlock}, + {3, FoldingRange.TokenPair}, + {3, FoldingRange.SpecialToken} + ] ranges = - merge_ranges_with_priorities([ - {1, indentation_ranges}, - {2, comment_block_ranges}, - {3, token_pair_ranges}, - {3, special_token_ranges} - ]) + passes_with_priority + |> Enum.map(fn {priority, pass} -> + ranges = ranges_from_pass(pass, input) + {priority, ranges} + end) + |> merge_ranges_with_priorities() {:ok, ranges} end @@ -76,6 +124,14 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRange do } end + defp ranges_from_pass(pass, input) do + with {:ok, ranges} <- pass.provide_ranges(input) do + ranges + else + _ -> [] + end + end + defp merge_ranges_with_priorities(range_lists_with_priorities) do range_lists_with_priorities |> Enum.flat_map(fn {priority, ranges} -> Enum.zip(Stream.cycle([priority]), ranges) end) diff --git a/apps/language_server/lib/language_server/providers/folding_range/comment_block.ex b/apps/language_server/lib/language_server/providers/folding_range/comment_block.ex index 0917d82ff..6a70f75a4 100644 --- a/apps/language_server/lib/language_server/providers/folding_range/comment_block.ex +++ b/apps/language_server/lib/language_server/providers/folding_range/comment_block.ex @@ -1,17 +1,39 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRange.CommentBlock do @moduledoc """ - Code folding based on comment blocks. + Code folding based on comment blocks - Note that this can create comment regions inside heredocs. - It's a little sloppy, but I don't think it's a big problem. + Note that this implementation can create comment ranges inside heredocs. + It's a little sloppy, but it shouldn't be very impactful. + We'd have to merge the token and line representations of the source text to + mitigate this issue, so we've left it as is for now. """ alias ElixirLS.LanguageServer.Providers.FoldingRange alias ElixirLS.LanguageServer.Providers.FoldingRange.Line @doc """ - Provides ranges for the source text based on the indentation level. - Note that we trim trailing empy rows from regions. + Provides ranges for the source text based on comment blocks. + + ## Example + + text = + \"\"\" + defmodule SomeModule do # 0 + def some_function() do # 1 + # I'm # 2 + # a # 3 + # comment block # 4 + nil # 5 + end # 6 + end # 7 + \"\"\" + + {:ok, ranges} = + text + |> FoldingRange.convert_text_to_input() + |> CommentBlock.provide_ranges() + + # ranges == [%{startLine: 2, endLine: 4, kind?: :comment}] """ @spec provide_ranges(FoldingRange.input()) :: {:ok, [FoldingRange.t()]} def provide_ranges(%{lines: lines}) do diff --git a/apps/language_server/lib/language_server/providers/folding_range/indentation.ex b/apps/language_server/lib/language_server/providers/folding_range/indentation.ex index 7486728a8..e7d160d1b 100644 --- a/apps/language_server/lib/language_server/providers/folding_range/indentation.ex +++ b/apps/language_server/lib/language_server/providers/folding_range/indentation.ex @@ -1,6 +1,9 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRange.Indentation do @moduledoc """ - Code folding based on indentation only. + Code folding based on indentation level + + Note that we trim trailing empty rows from regions. + See the example. """ alias ElixirLS.LanguageServer.Providers.FoldingRange @@ -8,7 +11,40 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRange.Indentation do @doc """ Provides ranges for the source text based on the indentation level. - Note that we trim trailing empy rows from regions. + + ## Example + + text = + \"\"\" + defmodule A do # 0 + def get_info(args) do # 1 + org = # 2 + args # 3 + |> Ecto.assoc(:organization) # 4 + |> Repo.one!() # 5 + + user = # 7 + org # 8 + |> Organization.user!() # 9 + + {:ok, %{org: org, user: user}} # 11 + end # 12 + end # 13 + \"\"\" + + {:ok, ranges} = + text + |> FoldingRange.convert_text_to_input() + |> Indentation.provide_ranges() + + # ranges == [ + # %{startLine: 0, endLine: 12, kind?: :region}, + # %{startLine: 1, endLine: 11, kind?: :region}, + # %{startLine: 2, endLine: 5, kind?: :region}, + # %{startLine: 7, endLine: 9, kind?: :region}, + # ] + + Note that the empty lines 6 and 10 do not appear in the inner most ranges. """ @spec provide_ranges(FoldingRange.input()) :: {:ok, [FoldingRange.t()]} def provide_ranges(%{lines: lines}) do diff --git a/apps/language_server/lib/language_server/providers/folding_range/special_token.ex b/apps/language_server/lib/language_server/providers/folding_range/special_token.ex index 3b59a8a55..8e2b3400d 100644 --- a/apps/language_server/lib/language_server/providers/folding_range/special_token.ex +++ b/apps/language_server/lib/language_server/providers/folding_range/special_token.ex @@ -1,6 +1,10 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRange.SpecialToken do @moduledoc """ - TODO: document this + Code folding based on "special" tokens. + + Several tokens, like `"..."`s, define ranges all on their own. + This module converts these tokens to ranges. + These ranges can be either `kind?: :comment` or `kind?: :region`. """ alias ElixirLS.LanguageServer.Providers.FoldingRange @@ -14,6 +18,35 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRange.SpecialToken do :sigil ] + @doc """ + Provides ranges based on "special" tokens + + ## Example + + text = + \"\"\" + defmodule A do # 0 + def hello() do # 1 + " + regular string # 3 + " + ' + charlist string # 6 + ' + end # 8 + end # 9 + \"\"\" + + {:ok, ranges} = + text + |> FoldingRange.convert_text_to_input() + |> SpecialToken.provide_ranges() + + # ranges == [ + # %{startLine: 2, endLine, 3, kind?: :region}, + # %{startLine: 5, endLine, 6, kind?: :region} + # ] + """ @spec provide_ranges([FoldingRange.input()]) :: {:ok, [FoldingRange.t()]} def provide_ranges(%{tokens: tokens}) do ranges = diff --git a/apps/language_server/lib/language_server/providers/folding_range/token.ex b/apps/language_server/lib/language_server/providers/folding_range/token.ex index 083fd00ef..cdf2dad6e 100644 --- a/apps/language_server/lib/language_server/providers/folding_range/token.ex +++ b/apps/language_server/lib/language_server/providers/folding_range/token.ex @@ -1,5 +1,8 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRange.Token do @moduledoc """ + This module normalizes the tokens provided by + + `ElixirSense.Core.Normalized.Tokenizer` """ alias ElixirSense.Core.Normalized.Tokenizer diff --git a/apps/language_server/lib/language_server/providers/folding_range/token_pairs.ex b/apps/language_server/lib/language_server/providers/folding_range/token_pairs.ex index e33e3d2e2..5316730b1 100644 --- a/apps/language_server/lib/language_server/providers/folding_range/token_pairs.ex +++ b/apps/language_server/lib/language_server/providers/folding_range/token_pairs.ex @@ -1,5 +1,12 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRange.TokenPair do @moduledoc """ + Code folding based on pairs of tokens + + Certain pairs of tokens, like `do` and `end`, natrually define ranges. + These ranges all have `kind?: :region`. + + Note that we exclude the line that the 2nd of the pair, e.g. `end`, is on. + This is so that when collapsed, both tokens are visible. """ alias ElixirLS.LanguageServer.Providers.FoldingRange @@ -20,6 +27,30 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRange.TokenPair do fn: [:end] } + @doc """ + Provides ranges based on token pairs + + ## Example + + text = + \"\"\" + defmodule Module do # 0 + def some_function() do # 1 + 4 # 2 + end # 3 + end # 4 + \"\"\" + + {:ok, ranges} = + text + |> FoldingRange.convert_text_to_input() + |> TokenPair.provide_ranges() + + # ranges == [ + # %{startLine: 0, endLine: 3, kind?: :region}, + # %{startLine: 1, endLine: 2, kind?: :region} + # ] + """ @spec provide_ranges([FoldingRange.input()]) :: {:ok, [FoldingRange.t()]} def provide_ranges(%{tokens: tokens}) do ranges = From 9e0cacfcf91ad56453a7d1895f404bb3bf111f79 Mon Sep 17 00:00:00 2001 From: Jason Axelson Date: Sat, 27 Feb 2021 13:16:40 -1000 Subject: [PATCH 60/61] Switch to doctests Also add a means to visualize folding range changes in the tests (only shows when there is a test failure) --- .../providers/folding_range.ex | 31 +++-- .../providers/folding_range/comment_block.ex | 34 +++--- .../providers/folding_range/indentation.ex | 54 ++++----- .../providers/folding_range/special_token.ex | 42 +++---- .../providers/folding_range/token_pairs.ex | 32 +++-- .../folding_range/comment_block_test.exs | 7 ++ .../folding_range/indentation_test.exs | 7 ++ .../folding_range/special_token_test.exs | 7 ++ .../folding_range/token_pairs_test.exs | 7 ++ .../test/providers/folding_range_test.exs | 110 +++++++++++------- 10 files changed, 185 insertions(+), 146 deletions(-) create mode 100644 apps/language_server/test/language_server/providers/folding_range/comment_block_test.exs create mode 100644 apps/language_server/test/language_server/providers/folding_range/indentation_test.exs create mode 100644 apps/language_server/test/language_server/providers/folding_range/special_token_test.exs create mode 100644 apps/language_server/test/language_server/providers/folding_range/token_pairs_test.exs diff --git a/apps/language_server/lib/language_server/providers/folding_range.ex b/apps/language_server/lib/language_server/providers/folding_range.ex index a68e04ff2..493946d15 100644 --- a/apps/language_server/lib/language_server/providers/folding_range.ex +++ b/apps/language_server/lib/language_server/providers/folding_range.ex @@ -69,23 +69,20 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRange do ## Example - text = \"\"\" - defmodule A do # 0 - def hello() do # 1 - :world # 2 - end # 3 - end # 4 - \"\"\" - - {:ok, ranges} = - text - |> convert_text_to_input() - |> provide() - - # ranges == [ - # %{startLine: 0, endLine: 3, kind?: :region}, - # %{startLine: 1, endLine: 2, kind?: :region} - # ] + iex> alias ElixirLS.LanguageServer.Providers.FoldingRange + iex> text = \""" + ...> defmodule A do # 0 + ...> def hello() do # 1 + ...> :world # 2 + ...> end # 3 + ...> end # 4 + ...> \""" + iex> FoldingRange.provide(%{text: text}) + {:ok, [ + %{startLine: 0, endLine: 3, kind?: :region}, + %{startLine: 1, endLine: 2, kind?: :region} + ]} + """ @spec provide(%{text: String.t()}) :: {:ok, [t()]} | {:error, String.t()} def provide(%{text: text}) do diff --git a/apps/language_server/lib/language_server/providers/folding_range/comment_block.ex b/apps/language_server/lib/language_server/providers/folding_range/comment_block.ex index 6a70f75a4..de5db3587 100644 --- a/apps/language_server/lib/language_server/providers/folding_range/comment_block.ex +++ b/apps/language_server/lib/language_server/providers/folding_range/comment_block.ex @@ -16,24 +16,22 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRange.CommentBlock do ## Example - text = - \"\"\" - defmodule SomeModule do # 0 - def some_function() do # 1 - # I'm # 2 - # a # 3 - # comment block # 4 - nil # 5 - end # 6 - end # 7 - \"\"\" - - {:ok, ranges} = - text - |> FoldingRange.convert_text_to_input() - |> CommentBlock.provide_ranges() - - # ranges == [%{startLine: 2, endLine: 4, kind?: :comment}] + iex> alias ElixirLS.LanguageServer.Providers.FoldingRange + iex> text = \""" + ...> defmodule SomeModule do # 0 + ...> def some_function() do # 1 + ...> # I'm # 2 + ...> # a # 3 + ...> # comment block # 4 + ...> nil # 5 + ...> end # 6 + ...> end # 7 + ...> \""" + iex> FoldingRange.convert_text_to_input(text) + iex> |> CommentBlock.provide_ranges() + {:ok, [ + %{startLine: 2, endLine: 4, kind?: :comment} + ]} """ @spec provide_ranges(FoldingRange.input()) :: {:ok, [FoldingRange.t()]} def provide_ranges(%{lines: lines}) do diff --git a/apps/language_server/lib/language_server/providers/folding_range/indentation.ex b/apps/language_server/lib/language_server/providers/folding_range/indentation.ex index e7d160d1b..6894ecfe8 100644 --- a/apps/language_server/lib/language_server/providers/folding_range/indentation.ex +++ b/apps/language_server/lib/language_server/providers/folding_range/indentation.ex @@ -14,35 +14,31 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRange.Indentation do ## Example - text = - \"\"\" - defmodule A do # 0 - def get_info(args) do # 1 - org = # 2 - args # 3 - |> Ecto.assoc(:organization) # 4 - |> Repo.one!() # 5 - - user = # 7 - org # 8 - |> Organization.user!() # 9 - - {:ok, %{org: org, user: user}} # 11 - end # 12 - end # 13 - \"\"\" - - {:ok, ranges} = - text - |> FoldingRange.convert_text_to_input() - |> Indentation.provide_ranges() - - # ranges == [ - # %{startLine: 0, endLine: 12, kind?: :region}, - # %{startLine: 1, endLine: 11, kind?: :region}, - # %{startLine: 2, endLine: 5, kind?: :region}, - # %{startLine: 7, endLine: 9, kind?: :region}, - # ] + iex> alias ElixirLS.LanguageServer.Providers.FoldingRange + iex> text = \""" + ...> defmodule A do # 0 + ...> def get_info(args) do # 1 + ...> org = # 2 + ...> args # 3 + ...> |> Ecto.assoc(:organization) # 4 + ...> |> Repo.one!() # 5 + ...> + ...> user = # 7 + ...> org # 8 + ...> |> Organization.user!() # 9 + ...> + ...> {:ok, %{org: org, user: user}} # 11 + ...> end # 12 + ...> end # 13 + ...> \""" + iex> FoldingRange.convert_text_to_input(text) + ...> |> FoldingRange.Indentation.provide_ranges() + {:ok, [ + %{startLine: 0, endLine: 12, kind?: :region}, + %{startLine: 1, endLine: 11, kind?: :region}, + %{startLine: 7, endLine: 9, kind?: :region}, + %{startLine: 2, endLine: 5, kind?: :region}, + ]} Note that the empty lines 6 and 10 do not appear in the inner most ranges. """ diff --git a/apps/language_server/lib/language_server/providers/folding_range/special_token.ex b/apps/language_server/lib/language_server/providers/folding_range/special_token.ex index 8e2b3400d..3254e22d7 100644 --- a/apps/language_server/lib/language_server/providers/folding_range/special_token.ex +++ b/apps/language_server/lib/language_server/providers/folding_range/special_token.ex @@ -23,29 +23,25 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRange.SpecialToken do ## Example - text = - \"\"\" - defmodule A do # 0 - def hello() do # 1 - " - regular string # 3 - " - ' - charlist string # 6 - ' - end # 8 - end # 9 - \"\"\" - - {:ok, ranges} = - text - |> FoldingRange.convert_text_to_input() - |> SpecialToken.provide_ranges() - - # ranges == [ - # %{startLine: 2, endLine, 3, kind?: :region}, - # %{startLine: 5, endLine, 6, kind?: :region} - # ] + iex> alias ElixirLS.LanguageServer.Providers.FoldingRange + iex> text = \""" + ...> defmodule A do # 0 + ...> def hello() do # 1 + ...> " + ...> regular string # 3 + ...> " + ...> ' + ...> charlist string # 6 + ...> ' + ...> end # 8 + ...> end # 9 + ...> \""" + iex> FoldingRange.convert_text_to_input(text) + ...> |> FoldingRange.SpecialToken.provide_ranges() + {:ok, [ + %{startLine: 5, endLine: 6, kind?: :region}, + %{startLine: 2, endLine: 3, kind?: :region}, + ]} """ @spec provide_ranges([FoldingRange.input()]) :: {:ok, [FoldingRange.t()]} def provide_ranges(%{tokens: tokens}) do diff --git a/apps/language_server/lib/language_server/providers/folding_range/token_pairs.ex b/apps/language_server/lib/language_server/providers/folding_range/token_pairs.ex index 5316730b1..48782d57f 100644 --- a/apps/language_server/lib/language_server/providers/folding_range/token_pairs.ex +++ b/apps/language_server/lib/language_server/providers/folding_range/token_pairs.ex @@ -32,24 +32,20 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRange.TokenPair do ## Example - text = - \"\"\" - defmodule Module do # 0 - def some_function() do # 1 - 4 # 2 - end # 3 - end # 4 - \"\"\" - - {:ok, ranges} = - text - |> FoldingRange.convert_text_to_input() - |> TokenPair.provide_ranges() - - # ranges == [ - # %{startLine: 0, endLine: 3, kind?: :region}, - # %{startLine: 1, endLine: 2, kind?: :region} - # ] + iex> alias ElixirLS.LanguageServer.Providers.FoldingRange + iex> text = \""" + ...> defmodule Module do # 0 + ...> def some_function() do # 1 + ...> 4 # 2 + ...> end # 3 + ...> end # 4 + ...> \""" + iex> FoldingRange.convert_text_to_input(text) + ...> |> TokenPair.provide_ranges() + {:ok, [ + %{startLine: 0, endLine: 3, kind?: :region}, + %{startLine: 1, endLine: 2, kind?: :region} + ]} """ @spec provide_ranges([FoldingRange.input()]) :: {:ok, [FoldingRange.t()]} def provide_ranges(%{tokens: tokens}) do diff --git a/apps/language_server/test/language_server/providers/folding_range/comment_block_test.exs b/apps/language_server/test/language_server/providers/folding_range/comment_block_test.exs new file mode 100644 index 000000000..6d7deff32 --- /dev/null +++ b/apps/language_server/test/language_server/providers/folding_range/comment_block_test.exs @@ -0,0 +1,7 @@ +defmodule ElixirLS.LanguageServer.Providers.FoldingRange.CommentBlockTest do + use ExUnit.Case + + alias ElixirLS.LanguageServer.Providers.FoldingRange.CommentBlock + + doctest(CommentBlock) +end diff --git a/apps/language_server/test/language_server/providers/folding_range/indentation_test.exs b/apps/language_server/test/language_server/providers/folding_range/indentation_test.exs new file mode 100644 index 000000000..a4decfbbe --- /dev/null +++ b/apps/language_server/test/language_server/providers/folding_range/indentation_test.exs @@ -0,0 +1,7 @@ +defmodule ElixirLS.LanguageServer.Providers.FoldingRange.IndentationTest do + use ExUnit.Case + + alias ElixirLS.LanguageServer.Providers.FoldingRange.Indentation + + doctest(Indentation) +end diff --git a/apps/language_server/test/language_server/providers/folding_range/special_token_test.exs b/apps/language_server/test/language_server/providers/folding_range/special_token_test.exs new file mode 100644 index 000000000..abccf2bbf --- /dev/null +++ b/apps/language_server/test/language_server/providers/folding_range/special_token_test.exs @@ -0,0 +1,7 @@ +defmodule ElixirLS.LanguageServer.Providers.FoldingRange.SpecialTokenTest do + use ExUnit.Case + + alias ElixirLS.LanguageServer.Providers.FoldingRange.SpecialToken + + doctest(SpecialToken) +end diff --git a/apps/language_server/test/language_server/providers/folding_range/token_pairs_test.exs b/apps/language_server/test/language_server/providers/folding_range/token_pairs_test.exs new file mode 100644 index 000000000..a4e092c91 --- /dev/null +++ b/apps/language_server/test/language_server/providers/folding_range/token_pairs_test.exs @@ -0,0 +1,7 @@ +defmodule ElixirLS.LanguageServer.Providers.FoldingRange.TokenPairTest do + use ExUnit.Case + + alias ElixirLS.LanguageServer.Providers.FoldingRange.TokenPair + + doctest(TokenPair) +end diff --git a/apps/language_server/test/providers/folding_range_test.exs b/apps/language_server/test/providers/folding_range_test.exs index 1e1676c16..5bcf92a98 100644 --- a/apps/language_server/test/providers/folding_range_test.exs +++ b/apps/language_server/test/providers/folding_range_test.exs @@ -3,6 +3,8 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRangeTest do alias ElixirLS.LanguageServer.Providers.FoldingRange + doctest(FoldingRange) + test "returns an :error tuple if input is not a source file" do assert {:error, _} = %{} |> FoldingRange.provide() end @@ -17,9 +19,9 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRangeTest do end # 3 end # 4 """ - test "basic test", %{ranges_result: ranges_result} do + test "basic test", %{ranges_result: ranges_result, text: text} do assert {:ok, ranges} = ranges_result - assert compare_condensed_ranges(ranges, [{0, 3}, {1, 2}]) + assert compare_condensed_ranges(ranges, [{0, 3}, {1, 2}], text) end @tag text: """ @@ -32,9 +34,9 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRangeTest do end # 6 end # 7 """ - test "consecutive matching levels", %{ranges_result: ranges_result} do + test "consecutive matching levels", %{ranges_result: ranges_result, text: text} do assert {:ok, ranges} = ranges_result - assert compare_condensed_ranges(ranges, [{0, 6}, {1, 5}, {3, 4}]) + assert compare_condensed_ranges(ranges, [{0, 6}, {1, 5}, {3, 4}], text) end @tag text: """ @@ -60,10 +62,10 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRangeTest do end # 19 end # 20 """ - test "complicated function", %{ranges_result: ranges_result} do + test "complicated function", %{ranges_result: ranges_result, text: text} do assert {:ok, ranges} = ranges_result expected = [{0, 19}, {1, 18}, {2, 17}, {3, 9}, {4, 7}, {11, 17}] - assert compare_condensed_ranges(ranges, expected) + assert compare_condensed_ranges(ranges, expected, text) end @tag text: """ @@ -82,9 +84,9 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRangeTest do end # 12 end # 13 """ - test "different complicated function", %{ranges_result: ranges_result} do + test "different complicated function", %{ranges_result: ranges_result, text: text} do assert {:ok, ranges} = ranges_result - assert compare_condensed_ranges(ranges, [{0, 12}, {1, 11}, {2, 5}, {7, 9}]) + assert compare_condensed_ranges(ranges, [{0, 12}, {1, 11}, {2, 5}, {7, 9}], text) end defp fold_via_indentation(%{text: text} = context) do @@ -107,9 +109,9 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRangeTest do end # 3 end # 4 """ - test "basic test", %{ranges_result: ranges_result} do + test "basic test", %{ranges_result: ranges_result, text: text} do assert {:ok, ranges} = ranges_result - assert compare_condensed_ranges(ranges, [{0, 3}, {1, 2}]) + assert compare_condensed_ranges(ranges, [{0, 3}, {1, 2}], text) end @tag text: """ @@ -119,9 +121,9 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRangeTest do end # 3 end # 4 """ - test "unusual indentation", %{ranges_result: ranges_result} do + test "unusual indentation", %{ranges_result: ranges_result, text: text} do assert {:ok, ranges} = ranges_result - assert compare_condensed_ranges(ranges, [{0, 3}, {1, 2}]) + assert compare_condensed_ranges(ranges, [{0, 3}, {1, 2}], text) end @tag text: """ @@ -135,9 +137,9 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRangeTest do end # 7 end # 8 """ - test "if-do-else-end", %{ranges_result: ranges_result} do + test "if-do-else-end", %{ranges_result: ranges_result, text: text} do assert {:ok, ranges} = ranges_result - assert compare_condensed_ranges(ranges, [{0, 7}, {1, 6}, {2, 3}, {4, 5}]) + assert compare_condensed_ranges(ranges, [{0, 7}, {1, 6}, {2, 3}, {4, 5}], text) end @tag text: """ @@ -160,10 +162,10 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRangeTest do end # 16 end # 17 """ - test "try block", %{ranges_result: ranges_result} do + test "try block", %{ranges_result: ranges_result, text: text} do assert {:ok, ranges} = ranges_result expected = [{0, 16}, {1, 15}, {2, 3}, {4, 6}, {7, 9}, {10, 12}, {13, 14}] - assert compare_condensed_ranges(ranges, expected) + assert compare_condensed_ranges(ranges, expected, text) end @tag text: """ @@ -181,9 +183,9 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRangeTest do end # 11 end # 12 """ - test "1 defmodule, 1 def, 1 case", %{ranges_result: ranges_result} do + test "1 defmodule, 1 def, 1 case", %{ranges_result: ranges_result, text: text} do assert {:ok, ranges} = ranges_result - assert compare_condensed_ranges(ranges, [{0, 11}, {1, 10}, {4, 9}]) + assert compare_condensed_ranges(ranges, [{0, 11}, {1, 10}, {4, 9}], text) end @tag text: """ @@ -197,9 +199,9 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRangeTest do end # 7 end # 8 """ - test "binaries", %{ranges_result: ranges_result} do + test "binaries", %{ranges_result: ranges_result, text: text} do assert {:ok, ranges} = ranges_result - assert compare_condensed_ranges(ranges, [{0, 7}, {1, 6}, {3, 5}]) + assert compare_condensed_ranges(ranges, [{0, 7}, {1, 6}, {3, 5}], text) end @tag text: """ @@ -211,9 +213,9 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRangeTest do @moduledoc "This is module B" # 5 end # 6 """ - test "2 defmodules in the top-level of file", %{ranges_result: ranges_result} do + test "2 defmodules in the top-level of file", %{ranges_result: ranges_result, text: text} do assert {:ok, ranges} = ranges_result - assert compare_condensed_ranges(ranges, [{0, 1}, {4, 5}]) + assert compare_condensed_ranges(ranges, [{0, 1}, {4, 5}], text) end @tag text: """ @@ -228,9 +230,9 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRangeTest do end # 8 end # 9 """ - test "1 defmodule, 1 def, 1 list", %{ranges_result: ranges_result} do + test "1 defmodule, 1 def, 1 list", %{ranges_result: ranges_result, text: text} do assert {:ok, ranges} = ranges_result - assert compare_condensed_ranges(ranges, [{0, 8}, {1, 7}, {2, 4}]) + assert compare_condensed_ranges(ranges, [{0, 8}, {1, 7}, {2, 4}], text) end @tag text: """ @@ -256,9 +258,9 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRangeTest do end # 19 end # 20 """ - test "complicated function", %{ranges_result: ranges_result} do + test "complicated function", %{ranges_result: ranges_result, text: text} do assert {:ok, ranges} = ranges_result - assert compare_condensed_ranges(ranges, [{0, 19}, {1, 18}, {2, 17}, {12, 14}]) + assert compare_condensed_ranges(ranges, [{0, 19}, {1, 18}, {2, 17}, {12, 14}], text) end defp fold_via_token_pairs(%{text: text} = context) do @@ -290,10 +292,10 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRangeTest do end # 12 end # 13 """ - test "@moduledoc, @doc, and stand-alone heredocs", %{ranges_result: ranges_result} do + test "@moduledoc, @doc, and stand-alone heredocs", %{ranges_result: ranges_result, text: text} do assert {:ok, ranges} = ranges_result expected = [{1, 2, :comment}, {5, 6, :comment}, {9, 10, :region}] - assert compare_condensed_ranges(ranges, expected) + assert compare_condensed_ranges(ranges, expected, text) end @tag text: """ @@ -314,9 +316,9 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRangeTest do end # 14 end # 15 """ - test "charlist heredocs", %{ranges_result: ranges_result} do + test "charlist heredocs", %{ranges_result: ranges_result, text: text} do assert {:ok, ranges} = ranges_result - assert compare_condensed_ranges(ranges, [{2, 3}, {5, 6}, {8, 9}, {11, 12}]) + assert compare_condensed_ranges(ranges, [{2, 3}, {5, 6}, {8, 9}, {11, 12}], text) end @tag text: """ @@ -349,10 +351,10 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRangeTest do end # 26 end # 27 """ - test "sigil delimiters", %{ranges_result: ranges_result} do + test "sigil delimiters", %{ranges_result: ranges_result, text: text} do assert {:ok, ranges} = ranges_result expected = [{2, 3}, {5, 6}, {8, 9}, {11, 12}, {14, 15}, {17, 18}, {20, 21}, {23, 24}] - assert compare_condensed_ranges(ranges, expected) + assert compare_condensed_ranges(ranges, expected, text) end @tag text: """ @@ -369,9 +371,9 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRangeTest do end # 10 end # 11 """ - test "@doc with ~S sigil", %{ranges_result: ranges_result} do + test "@doc with ~S sigil", %{ranges_result: ranges_result, text: text} do assert {:ok, ranges} = ranges_result - assert compare_condensed_ranges(ranges, [{1, 2, :comment}, {5, 6, :comment}]) + assert compare_condensed_ranges(ranges, [{1, 2, :comment}, {5, 6, :comment}], text) end defp fold_via_special_tokens(%{text: text} = context) do @@ -395,9 +397,9 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRangeTest do end # 4 end # 5 """ - test "no single line comment blocks", %{ranges_result: ranges_result} do + test "no single line comment blocks", %{ranges_result: ranges_result, text: text} do assert {:ok, ranges} = ranges_result - assert compare_condensed_ranges(ranges, []) + assert compare_condensed_ranges(ranges, [], text) end @tag text: """ @@ -412,9 +414,9 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRangeTest do defp do_hello(), do: :world # 8 end # 9 """ - test "@moduledoc, @doc, and stand-alone heredocs", %{ranges_result: ranges_result} do + test "@moduledoc, @doc, and stand-alone heredocs", %{ranges_result: ranges_result, text: text} do assert {:ok, ranges} = ranges_result - assert compare_condensed_ranges(ranges, [{5, 7}]) + assert compare_condensed_ranges(ranges, [{5, 7}], text) end defp fold_via_comment_blocks(%{text: text} = context) do @@ -459,7 +461,7 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRangeTest do end # 25 end # 26 """ - test "complicated function", %{ranges_result: ranges_result} do + test "complicated function", %{ranges_result: ranges_result, text: text} do assert {:ok, ranges} = ranges_result expected = [ @@ -474,7 +476,7 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRangeTest do {18, 20} ] - assert compare_condensed_ranges(ranges, expected) + assert compare_condensed_ranges(ranges, expected, text) end defp fold_text(%{text: _text} = context) do @@ -483,7 +485,7 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRangeTest do end end - defp compare_condensed_ranges(result, expected_condensed) do + defp compare_condensed_ranges(result, expected_condensed, text) do result_condensed = result |> Enum.map(fn @@ -517,6 +519,32 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRangeTest do end) |> Enum.unzip() + if result_condensed != expected_condensed do + visualize_folding(text, result_condensed) + end + assert result_condensed == expected_condensed end + + def visualize_folding(nil, _), do: :ok + + def visualize_folding(text, result_condensed) do + lines = + String.split(text, "\n") + |> Enum.with_index() + |> Enum.map(fn {line, index} -> + String.pad_leading(to_string(index), 2, " ") <> ": " <> line + end) + + result_condensed + |> Enum.map(fn {line_start, line_end, :any} -> + out = + Enum.slice(lines, line_start, line_end - line_start + 2) + |> Enum.join("\n") + + IO.puts("Folding lines #{line_start}, #{line_end}:") + IO.puts(out) + IO.puts("\n") + end) + end end From 35d3fd5ab4dcc63d18c088bb8ed3a89be21c04ea Mon Sep 17 00:00:00 2001 From: Jason Axelson Date: Sat, 20 Mar 2021 09:58:24 -1000 Subject: [PATCH 61/61] Update apps/language_server/lib/language_server/providers/folding_range/token_pairs.ex --- .../lib/language_server/providers/folding_range/token_pairs.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/language_server/lib/language_server/providers/folding_range/token_pairs.ex b/apps/language_server/lib/language_server/providers/folding_range/token_pairs.ex index 48782d57f..2e481b1bb 100644 --- a/apps/language_server/lib/language_server/providers/folding_range/token_pairs.ex +++ b/apps/language_server/lib/language_server/providers/folding_range/token_pairs.ex @@ -20,7 +20,7 @@ defmodule ElixirLS.LanguageServer.Providers.FoldingRange.TokenPair do # do blocks do: [:block_identifier, :end], block_identifier: [:block_identifier, :end], - # other special forms + # other special forms that are not covered by :block_identifier with: [:do], for: [:do], case: [:do],