From 53c9ed64289c12fca12feeaf348aa353191d6418 Mon Sep 17 00:00:00 2001 From: Nikola Jichev Date: Thu, 25 Apr 2024 17:59:43 +0300 Subject: [PATCH 1/2] feat: add a folding provider --- lib/next_ls.ex | 20 +++++++++ lib/next_ls/folding_range.ex | 64 +++++++++++++++++++++++++++++ test/next_ls/folding_range_test.exs | 29 +++++++++++++ 3 files changed, 113 insertions(+) create mode 100644 lib/next_ls/folding_range.ex create mode 100644 test/next_ls/folding_range_test.exs diff --git a/lib/next_ls.ex b/lib/next_ls.ex index 4a5cc898..92654a4f 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..4e1e87b1 --- /dev/null +++ b/lib/next_ls/folding_range.ex @@ -0,0 +1,64 @@ +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.character) <> " ..." + } + end + + Enum.map(foldings, create_folding) + 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..0dfd5270 --- /dev/null +++ b/test/next_ls/folding_range_test.exs @@ -0,0 +1,29 @@ +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 + + defp create_folding(code) do + code + |> String.split("\n") + |> FoldingRange.new() + end +end From c28a07546dffaabc16eec2526aeb480a1a8276f2 Mon Sep 17 00:00:00 2001 From: Nikola Jichev Date: Fri, 3 May 2024 17:04:52 +0300 Subject: [PATCH 2/2] refactor: add more tests --- lib/next_ls.ex | 2 +- lib/next_ls/folding_range.ex | 8 +++- test/next_ls/folding_range_test.exs | 59 +++++++++++++++++++++++++---- 3 files changed, 58 insertions(+), 11 deletions(-) diff --git a/lib/next_ls.ex b/lib/next_ls.ex index 92654a4f..8ad85548 100644 --- a/lib/next_ls.ex +++ b/lib/next_ls.ex @@ -158,7 +158,7 @@ defmodule NextLS do "alias-refactor" ] }, - folding_range_provider: true, + folding_range_provider: true, hover_provider: true, workspace_symbol_provider: true, document_symbol_provider: true, diff --git a/lib/next_ls/folding_range.ex b/lib/next_ls/folding_range.ex index 4e1e87b1..86baf160 100644 --- a/lib/next_ls/folding_range.ex +++ b/lib/next_ls/folding_range.ex @@ -14,6 +14,7 @@ defmodule NextLS.FoldingRange do |> Z.zip() |> Z.traverse([], fn tree, acc -> node = Z.node(tree) + if is_foldable?(node) do {tree, [node | acc]} else @@ -23,17 +24,20 @@ defmodule NextLS.FoldingRange do 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.character) <> " ..." + collapsed_text: Enum.at(text, range.start.line) <> " ..." } end - Enum.map(foldings, create_folding) + foldings + |> Enum.map(create_folding) + |> Enum.reverse() end end diff --git a/test/next_ls/folding_range_test.exs b/test/next_ls/folding_range_test.exs index 0dfd5270..846ca4be 100644 --- a/test/next_ls/folding_range_test.exs +++ b/test/next_ls/folding_range_test.exs @@ -11,14 +11,57 @@ defmodule NextLS.AliasTest do 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) + 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