diff --git a/lib/next_ls.ex b/lib/next_ls.ex index 4a5cc898..8ad85548 100644 --- a/lib/next_ls.ex +++ b/lib/next_ls.ex @@ -21,6 +21,7 @@ defmodule NextLS do alias GenLSP.Requests.TextDocumentCompletion alias GenLSP.Requests.TextDocumentDefinition alias GenLSP.Requests.TextDocumentDocumentSymbol + alias GenLSP.Requests.TextDocumentFoldingRange alias GenLSP.Requests.TextDocumentFormatting alias GenLSP.Requests.TextDocumentHover alias GenLSP.Requests.TextDocumentReferences @@ -157,6 +158,7 @@ defmodule NextLS do "alias-refactor" ] }, + folding_range_provider: true, hover_provider: true, workspace_symbol_provider: true, document_symbol_provider: true, @@ -523,6 +525,24 @@ defmodule NextLS do {:reply, symbols, lsp} end + def handle_request(%TextDocumentFoldingRange{params: %{text_document: %{uri: uri}}}, lsp) do + document = lsp.assigns.documents[uri] + + resp = + if is_list(document) do + {:reply, NextLS.FoldingRange.new(document), lsp} + else + GenLSP.warning( + lsp, + "[Next LS] The file #{uri} was not found in the server's process state. Something must have gone wrong when opening, changing, or saving the file." + ) + + {:reply, nil, lsp} + end + + resp + end + def handle_request(%TextDocumentFormatting{params: %{text_document: %{uri: uri}}}, lsp) do document = lsp.assigns.documents[uri] diff --git a/lib/next_ls/folding_range.ex b/lib/next_ls/folding_range.ex new file mode 100644 index 00000000..86baf160 --- /dev/null +++ b/lib/next_ls/folding_range.ex @@ -0,0 +1,68 @@ +defmodule NextLS.FoldingRange do + @moduledoc "Traverses the AST and creates folding ranges" + + alias GenLSP.Structures.FoldingRange + alias GenLSP.Structures.Position + alias GenLSP.Structures.Range + alias Sourceror.Zipper, as: Z + + @spec new(text :: String.t()) :: [FoldingRange.t()] + def new(text) do + with {:ok, ast} <- parse(text) do + {_ast, foldings} = + ast + |> Z.zip() + |> Z.traverse([], fn tree, acc -> + node = Z.node(tree) + + if is_foldable?(node) do + {tree, [node | acc]} + else + {tree, acc} + end + end) + + create_folding = fn node -> + range = make_range(node) + + %FoldingRange{ + kind: "region", + start_line: range.start.line, + start_character: range.start.character, + end_line: range.end.line, + end_character: range.end.character, + collapsed_text: Enum.at(text, range.start.line) <> " ..." + } + end + + foldings + |> Enum.map(create_folding) + |> Enum.reverse() + end + end + + defp parse(lines) do + lines + |> Enum.join("\n") + |> Spitfire.parse(literal_encoder: &{:ok, {:__block__, &2, [&1]}}) + |> case do + {:error, ast, _errors} -> + {:ok, ast} + + other -> + other + end + end + + defp is_foldable?({_, _, [_name, [{{:__block__, _, [:do]}, _}]]}), do: true + defp is_foldable?(_), do: false + + defp make_range({_, ctx, _}) do + eoe = ctx[:end_of_expression] + + %Range{ + start: %Position{line: ctx[:line] - 1, character: ctx[:column] - 1}, + end: %Position{line: eoe[:line] - 1, character: eoe[:column] - 1} + } + end +end diff --git a/test/next_ls/folding_range_test.exs b/test/next_ls/folding_range_test.exs new file mode 100644 index 00000000..846ca4be --- /dev/null +++ b/test/next_ls/folding_range_test.exs @@ -0,0 +1,72 @@ +defmodule NextLS.AliasTest do + use ExUnit.Case, async: true + + alias GenLSP.Structures.FoldingRange, as: FR + alias NextLS.FoldingRange + + test "creates a folding range for modules" do + code = """ + defmodule MyModule do + # Some text here + end + """ + + assert [ + %FR{ + start_line: 0, + start_character: 0, + end_line: 2, + end_character: 3, + kind: "region", + collapsed_text: "defmodule MyModule do ..." + } + ] = create_folding(code) + end + + test "creates a folding range for functions" do + code = """ + defmodule MyModule do + def foo(a, b) do + a + b + end + + defp bar(a, b, c) do + a + b + c + end + end + """ + + assert [ + %FR{ + start_line: 0, + start_character: 0, + end_line: 8, + end_character: 3, + kind: "region", + collapsed_text: "defmodule MyModule do ..." + }, + %FR{ + start_line: 1, + start_character: 2, + end_line: 3, + end_character: 5, + kind: "region", + collapsed_text: " def foo(a, b) do ..." + }, + %FR{ + start_line: 5, + start_character: 2, + end_line: 7, + end_character: 5, + kind: "region", + collapsed_text: " defp bar(a, b, c) do ..." + } + ] = create_folding(code) + end + + defp create_folding(code) do + code + |> String.split("\n") + |> FoldingRange.new() + end +end