From 1b54fcfc16d6119c7c3aecb433e48b3f62f7f5b5 Mon Sep 17 00:00:00 2001 From: Mitchell Hanberg Date: Wed, 24 Apr 2024 19:09:51 -0400 Subject: [PATCH 1/3] feat: undefined function code action When you have an undefined local function, this code action allows you to create a private function stub for the function. --- lib/next_ls/extensions/elixir_extension.ex | 13 +- .../elixir_extension/code_action.ex | 4 + .../code_action/undefined_function.ex | 82 ++++++++++ .../code_action/require_test.exs | 6 +- .../code_action/undefined_function_test.exs | 150 ++++++++++++++++++ 5 files changed, 245 insertions(+), 10 deletions(-) create mode 100644 lib/next_ls/extensions/elixir_extension/code_action/undefined_function.ex create mode 100644 test/next_ls/extensions/elixir_extension/code_action/undefined_function_test.exs diff --git a/lib/next_ls/extensions/elixir_extension.ex b/lib/next_ls/extensions/elixir_extension.ex index a6c7166a..0d161e3e 100644 --- a/lib/next_ls/extensions/elixir_extension.ex +++ b/lib/next_ls/extensions/elixir_extension.ex @@ -32,14 +32,6 @@ defmodule NextLS.ElixirExtension do DiagnosticCache.clear(state.cache, :elixir) for d <- diagnostics do - # TODO: some compiler diagnostics only have the line number - # but we want to only highlight the source code, so we - # need to read the text of the file (either from the lsp cache - # if the source code is "open", or read from disk) and calculate the - # column of the first non-whitespace character. - # - # it is not clear to me whether the LSP process or the extension should - # be responsible for this. The open documents live in the LSP process DiagnosticCache.put(state.cache, :elixir, d.file, %GenLSP.Structures.Diagnostic{ severity: severity(d.severity), message: IO.iodata_to_binary(d.message), @@ -115,6 +107,7 @@ defmodule NextLS.ElixirExtension do @unused_variable ~r/variable\s\"[^\"]+\"\sis\sunused/ @require_module ~r/you\smust\srequire/ + @undefined_local_function ~r/undefined function (?.*)\/(?\d) \(expected (?.*) to define such a function or for it to be imported, but none are available\)/ defp metadata(diagnostic) do base = %{"namespace" => "elixir"} @@ -125,6 +118,10 @@ defmodule NextLS.ElixirExtension do is_binary(diagnostic.message) and diagnostic.message =~ @require_module -> Map.put(base, "type", "require") + is_binary(diagnostic.message) and diagnostic.message =~ @undefined_local_function -> + info = Regex.named_captures(@undefined_local_function, diagnostic.message) + base |> Map.put("type", "undefined-function") |> Map.put("info", info) + true -> base end diff --git a/lib/next_ls/extensions/elixir_extension/code_action.ex b/lib/next_ls/extensions/elixir_extension/code_action.ex index ea1310b7..d7b92a67 100644 --- a/lib/next_ls/extensions/elixir_extension/code_action.ex +++ b/lib/next_ls/extensions/elixir_extension/code_action.ex @@ -5,6 +5,7 @@ defmodule NextLS.ElixirExtension.CodeAction do alias NextLS.CodeActionable.Data alias NextLS.ElixirExtension.CodeAction.Require + alias NextLS.ElixirExtension.CodeAction.UndefinedFunction alias NextLS.ElixirExtension.CodeAction.UnusedVariable @impl true @@ -16,6 +17,9 @@ defmodule NextLS.ElixirExtension.CodeAction do %{"type" => "require"} -> Require.new(data.diagnostic, data.document, data.uri) + %{"type" => "undefined-function"} -> + UndefinedFunction.new(data.diagnostic, data.document, data.uri) + _ -> [] end diff --git a/lib/next_ls/extensions/elixir_extension/code_action/undefined_function.ex b/lib/next_ls/extensions/elixir_extension/code_action/undefined_function.ex new file mode 100644 index 00000000..88bd2be0 --- /dev/null +++ b/lib/next_ls/extensions/elixir_extension/code_action/undefined_function.ex @@ -0,0 +1,82 @@ +defmodule NextLS.ElixirExtension.CodeAction.UndefinedFunction do + @moduledoc false + + alias GenLSP.Structures.CodeAction + alias GenLSP.Structures.Diagnostic + alias GenLSP.Structures.Range + alias GenLSP.Structures.TextEdit + alias GenLSP.Structures.WorkspaceEdit + alias NextLS.ASTHelpers + + def new(diagnostic, text, uri) do + %Diagnostic{range: range, data: %{"info" => info}} = diagnostic + + with {:ok, ast} <- + text + |> Enum.join("\n") + |> Spitfire.parse( + literal_encoder: + &{:ok, + { + :__block__, + &2, + [&1] + }} + ), + {:ok, {:defmodule, meta, _} = defm} <- ASTHelpers.get_surrounding_module(ast, range.start), + indentation <- get_indent(text, defm) do + position = %GenLSP.Structures.Position{ + line: meta[:end][:line] - 1, + character: 0 + } + + %{ + "name" => name, + "arity" => arity + } = info + + params = if arity == "0", do: "", else: Enum.map_join(1..String.to_integer(arity), ", ", fn i -> "param#{i}" end) + + new_text = """ + + #{indentation}defp #{name}(#{params}) do + + #{indentation}end + """ + + [ + %CodeAction{ + title: "Create local private function #{info["name"]}/#{info["arity"]}", + diagnostics: [diagnostic], + edit: %WorkspaceEdit{ + changes: %{ + uri => [ + %TextEdit{ + new_text: new_text, + range: %Range{ + start: position, + end: position + } + } + ] + } + } + } + ] + end + end + + @one_indentation_level " " + @indent ~r/^(\s*).*/ + defp get_indent(text, {_, defm_context, _}) do + line = defm_context[:line] - 1 + + indent = + text + |> Enum.at(line) + |> then(&Regex.run(@indent, &1)) + |> List.last() + + indent <> @one_indentation_level + end +end diff --git a/test/next_ls/extensions/elixir_extension/code_action/require_test.exs b/test/next_ls/extensions/elixir_extension/code_action/require_test.exs index 39e5942b..e960119c 100644 --- a/test/next_ls/extensions/elixir_extension/code_action/require_test.exs +++ b/test/next_ls/extensions/elixir_extension/code_action/require_test.exs @@ -21,7 +21,7 @@ defmodule NextLS.ElixirExtension.RequireTest do "\n" ) - start = %Position{character: 0, line: 1} + start = %Position{character: 11, line: 2} diagnostic = %GenLSP.Structures.Diagnostic{ data: %{"namespace" => "elixir", "type" => "require"}, @@ -40,12 +40,14 @@ defmodule NextLS.ElixirExtension.RequireTest do assert [diagnostic] == code_action.diagnostics assert code_action.title == "Add missing require for Logger" + edit_position = %GenLSP.Structures.Position{line: 1, character: 0} + assert %WorkspaceEdit{ changes: %{ ^uri => [ %TextEdit{ new_text: " require Logger\n", - range: %Range{start: ^start, end: ^start} + range: %Range{start: ^edit_position, end: ^edit_position} } ] } diff --git a/test/next_ls/extensions/elixir_extension/code_action/undefined_function_test.exs b/test/next_ls/extensions/elixir_extension/code_action/undefined_function_test.exs new file mode 100644 index 00000000..72d0f7ed --- /dev/null +++ b/test/next_ls/extensions/elixir_extension/code_action/undefined_function_test.exs @@ -0,0 +1,150 @@ +defmodule NextLS.ElixirExtension.UndefinedFunctionTest do + use ExUnit.Case, async: true + + alias GenLSP.Structures.CodeAction + alias GenLSP.Structures.Position + alias GenLSP.Structures.Range + alias GenLSP.Structures.TextEdit + alias GenLSP.Structures.WorkspaceEdit + alias NextLS.ElixirExtension.CodeAction.UndefinedFunction + + test "in outer module creates new private function inside current module" do + text = + String.split( + """ + defmodule Test.Foo do + defmodule Bar do + def run() do + :ok + end + end + + def hello() do + bar(1, 2) + end + + defmodule Baz do + def run() do + :error + end + end + end + """, + "\n" + ) + + start = %Position{character: 4, line: 8} + + diagnostic = %GenLSP.Structures.Diagnostic{ + data: %{ + "namespace" => "elixir", + "type" => "undefined-function", + "info" => %{ + "name" => "bar", + "arity" => "2", + "module" => "Elixir.Test.Foo" + } + }, + message: + "undefined function bar/2 (expected Test.Foo to define such a function or for it to be imported, but none are available)", + source: "Elixir", + range: %GenLSP.Structures.Range{ + start: start, + end: %{start | character: 6} + } + } + + uri = "file:///home/owner/my_project/hello.ex" + + assert [code_action] = UndefinedFunction.new(diagnostic, text, uri) + assert %CodeAction{} = code_action + assert [diagnostic] == code_action.diagnostics + assert code_action.title == "Create local private function bar/2" + + edit_position = %Position{line: 16, character: 0} + + assert %WorkspaceEdit{ + changes: %{ + ^uri => [ + %TextEdit{ + new_text: """ + + defp bar(param1, param2) do + + end + """, + range: %Range{start: ^edit_position, end: ^edit_position} + } + ] + } + } = code_action.edit + end + + test "in inner module creates new private function inside current module" do + text = + String.split( + """ + defmodule Test.Foo do + defmodule Bar do + def run() do + bar(1, 2) + end + end + + defmodule Baz do + def run() do + :error + end + end + end + """, + "\n" + ) + + start = %Position{character: 6, line: 3} + + diagnostic = %GenLSP.Structures.Diagnostic{ + data: %{ + "namespace" => "elixir", + "type" => "undefined-function", + "info" => %{ + "name" => "bar", + "arity" => "2", + "module" => "Elixir.Test.Foo.Bar" + } + }, + message: + "undefined function bar/2 (expected Test.Foo to define such a function or for it to be imported, but none are available)", + source: "Elixir", + range: %GenLSP.Structures.Range{ + start: start, + end: %{start | character: 9} + } + } + + uri = "file:///home/owner/my_project/hello.ex" + + assert [code_action] = UndefinedFunction.new(diagnostic, text, uri) + assert %CodeAction{} = code_action + assert [diagnostic] == code_action.diagnostics + assert code_action.title == "Create local private function bar/2" + + edit_position = %Position{line: 5, character: 0} + + assert %WorkspaceEdit{ + changes: %{ + ^uri => [ + %TextEdit{ + new_text: """ + + defp bar(param1, param2) do + + end + """, + range: %Range{start: ^edit_position, end: ^edit_position} + } + ] + } + } = code_action.edit + end +end From 7dbd09036dfeedab31cbe943a3d4418fdea2f9d8 Mon Sep 17 00:00:00 2001 From: Mitchell Hanberg Date: Wed, 24 Apr 2024 19:24:28 -0400 Subject: [PATCH 2/3] fixup! feat: undefined function code action --- .../code_action/undefined_function.ex | 48 +++++++++---------- .../code_action/undefined_function_test.exs | 30 ++++++++++-- 2 files changed, 47 insertions(+), 31 deletions(-) diff --git a/lib/next_ls/extensions/elixir_extension/code_action/undefined_function.ex b/lib/next_ls/extensions/elixir_extension/code_action/undefined_function.ex index 88bd2be0..738386ba 100644 --- a/lib/next_ls/extensions/elixir_extension/code_action/undefined_function.ex +++ b/lib/next_ls/extensions/elixir_extension/code_action/undefined_function.ex @@ -9,44 +9,25 @@ defmodule NextLS.ElixirExtension.CodeAction.UndefinedFunction do alias NextLS.ASTHelpers def new(diagnostic, text, uri) do - %Diagnostic{range: range, data: %{"info" => info}} = diagnostic + %Diagnostic{range: range, data: %{"info" => %{"name" => name, "arity" => arity}}} = diagnostic with {:ok, ast} <- text |> Enum.join("\n") - |> Spitfire.parse( - literal_encoder: - &{:ok, - { - :__block__, - &2, - [&1] - }} - ), - {:ok, {:defmodule, meta, _} = defm} <- ASTHelpers.get_surrounding_module(ast, range.start), - indentation <- get_indent(text, defm) do + |> Spitfire.parse(literal_encoder: &{:ok, {:__block__, &2, [&1]}}), + {:ok, {:defmodule, meta, _} = defm} <- ASTHelpers.get_surrounding_module(ast, range.start) do + indentation = get_indent(text, defm) + position = %GenLSP.Structures.Position{ line: meta[:end][:line] - 1, character: 0 } - %{ - "name" => name, - "arity" => arity - } = info - params = if arity == "0", do: "", else: Enum.map_join(1..String.to_integer(arity), ", ", fn i -> "param#{i}" end) - new_text = """ - - #{indentation}defp #{name}(#{params}) do - - #{indentation}end - """ - - [ + action = fn title, new_text -> %CodeAction{ - title: "Create local private function #{info["name"]}/#{info["arity"]}", + title: title, diagnostics: [diagnostic], edit: %WorkspaceEdit{ changes: %{ @@ -62,6 +43,21 @@ defmodule NextLS.ElixirExtension.CodeAction.UndefinedFunction do } } } + end + + [ + action.("Create public function #{name}/#{arity}", """ + + #{indentation}def #{name}(#{params}) do + + #{indentation}end + """), + action.("Create private function #{name}/#{arity}", """ + + #{indentation}defp #{name}(#{params}) do + + #{indentation}end + """) ] end end diff --git a/test/next_ls/extensions/elixir_extension/code_action/undefined_function_test.exs b/test/next_ls/extensions/elixir_extension/code_action/undefined_function_test.exs index 72d0f7ed..26195b0e 100644 --- a/test/next_ls/extensions/elixir_extension/code_action/undefined_function_test.exs +++ b/test/next_ls/extensions/elixir_extension/code_action/undefined_function_test.exs @@ -56,10 +56,30 @@ defmodule NextLS.ElixirExtension.UndefinedFunctionTest do uri = "file:///home/owner/my_project/hello.ex" - assert [code_action] = UndefinedFunction.new(diagnostic, text, uri) - assert %CodeAction{} = code_action - assert [diagnostic] == code_action.diagnostics - assert code_action.title == "Create local private function bar/2" + assert [public, private] = UndefinedFunction.new(diagnostic, text, uri) + assert [diagnostic] == public.diagnostics + assert public.title == "Create public function bar/2" + + edit_position = %Position{line: 16, character: 0} + + assert %WorkspaceEdit{ + changes: %{ + ^uri => [ + %TextEdit{ + new_text: """ + + def bar(param1, param2) do + + end + """, + range: %Range{start: ^edit_position, end: ^edit_position} + } + ] + } + } = public.edit + + assert [diagnostic] == private.diagnostics + assert private.title == "Create private function bar/2" edit_position = %Position{line: 16, character: 0} @@ -77,7 +97,7 @@ defmodule NextLS.ElixirExtension.UndefinedFunctionTest do } ] } - } = code_action.edit + } = private.edit end test "in inner module creates new private function inside current module" do From 4f472472312d06a7a1f0268a46837a68ef6bfd27 Mon Sep 17 00:00:00 2001 From: Mitchell Hanberg Date: Thu, 25 Apr 2024 10:05:23 -0400 Subject: [PATCH 3/3] fixup! feat: undefined function code action --- .../elixir_extension/code_action/undefined_function_test.exs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/next_ls/extensions/elixir_extension/code_action/undefined_function_test.exs b/test/next_ls/extensions/elixir_extension/code_action/undefined_function_test.exs index 26195b0e..4dc3180e 100644 --- a/test/next_ls/extensions/elixir_extension/code_action/undefined_function_test.exs +++ b/test/next_ls/extensions/elixir_extension/code_action/undefined_function_test.exs @@ -144,10 +144,10 @@ defmodule NextLS.ElixirExtension.UndefinedFunctionTest do uri = "file:///home/owner/my_project/hello.ex" - assert [code_action] = UndefinedFunction.new(diagnostic, text, uri) + assert [_, code_action] = UndefinedFunction.new(diagnostic, text, uri) assert %CodeAction{} = code_action assert [diagnostic] == code_action.diagnostics - assert code_action.title == "Create local private function bar/2" + assert code_action.title == "Create private function bar/2" edit_position = %Position{line: 5, character: 0}