diff --git a/README.md b/README.md index 5341d639..cfead8de 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ Still in heavy development, currently supporting the following features: - Workspace Symbols - Document Symbols - Go To Definition +- Hover ## Editor Support diff --git a/lib/next_ls.ex b/lib/next_ls.ex index e79d1720..e659df9f 100644 --- a/lib/next_ls.ex +++ b/lib/next_ls.ex @@ -23,6 +23,7 @@ defmodule NextLS do TextDocumentDocumentSymbol, TextDocumentDefinition, TextDocumentFormatting, + TextDocumentHover, WorkspaceSymbol } @@ -44,6 +45,7 @@ defmodule NextLS do } alias NextLS.DiagnosticCache + alias NextLS.ReferenceTable alias NextLS.Runtime alias NextLS.SymbolTable alias NextLS.Definition @@ -57,7 +59,8 @@ defmodule NextLS do :dynamic_supervisor, :extensions, :extension_registry, - :symbol_table + :symbol_table, + :reference_table ]) GenLSP.start_link(__MODULE__, args, opts) @@ -72,6 +75,7 @@ defmodule NextLS do extensions = Keyword.get(args, :extensions, [NextLS.ElixirExtension]) cache = Keyword.fetch!(args, :cache) symbol_table = Keyword.fetch!(args, :symbol_table) + reference_table = Keyword.fetch!(args, :reference_table) {:ok, logger} = DynamicSupervisor.start_child(dynamic_supervisor, {NextLS.Logger, lsp: lsp}) {:ok, @@ -82,6 +86,7 @@ defmodule NextLS do cache: cache, logger: logger, symbol_table: symbol_table, + reference_table: reference_table, task_supervisor: task_supervisor, runtime_task_supervisor: runtime_task_supervisor, dynamic_supervisor: dynamic_supervisor, @@ -105,7 +110,8 @@ defmodule NextLS do document_formatting_provider: true, workspace_symbol_provider: true, document_symbol_provider: true, - definition_provider: true + definition_provider: true, + hover_provider: true }, server_info: %{name: "NextLS"} }, assign(lsp, root_uri: root_uri)} @@ -117,7 +123,7 @@ defmodule NextLS do URI.parse(uri).path, {position.line + 1, position.character + 1}, :symbol_table, - :reference_table + lsp.assigns.reference_table ) do nil -> nil @@ -159,6 +165,19 @@ defmodule NextLS do {:reply, symbols, lsp} end + def handle_request(%TextDocumentHover{params: %{text_document: %{uri: uri}, position: position}}, lsp) do + hover = + try do + NextLS.Hover.fetch(lsp, uri, position) + rescue + e -> + GenLSP.error(lsp, Exception.format_banner(:error, e, __STACKTRACE__)) + nil + end + + {:reply, hover, lsp} + end + def handle_request(%WorkspaceSymbol{params: %{query: query}}, lsp) do filter = fn sym -> if query == "" do @@ -237,6 +256,7 @@ defmodule NextLS do def handle_request(%Shutdown{}, lsp) do SymbolTable.close(lsp.assigns.symbol_table) + ReferenceTable.close(lsp.assigns.reference_table) {:reply, nil, assign(lsp, exit_code: 0)} end @@ -389,7 +409,7 @@ defmodule NextLS do end def handle_info({{:tracer, :reference}, payload}, lsp) do - SymbolTable.put_reference(lsp.assigns.symbol_table, payload) + ReferenceTable.put_reference(lsp.assigns.reference_table, payload) {:noreply, lsp} end diff --git a/lib/next_ls/definition.ex b/lib/next_ls/definition.ex index bb4581ce..452b5a02 100644 --- a/lib/next_ls/definition.ex +++ b/lib/next_ls/definition.ex @@ -1,22 +1,8 @@ defmodule NextLS.Definition do - def fetch(file, {line, col}, dets_symbol_table, dets_ref_table) do - ref = - :dets.select( - dets_ref_table, - [ - {{{:"$1", {{:"$2", :"$3"}, {:"$4", :"$5"}}}, :"$6"}, - [ - {:andalso, - {:andalso, {:andalso, {:andalso, {:==, :"$1", file}, {:"=<", :"$2", line}}, {:"=<", :"$3", col}}, - {:"=<", line, :"$4"}}, {:"=<", col, :"$5"}} - ], [:"$6"]} - ] - ) + alias NextLS.ReferenceTable - # :dets.traverse(dets_symbol_table, fn x -> {:continue, x} end) |> dbg - # :dets.traverse(dets_ref_table, fn x -> {:continue, x} end) |> dbg - - # dbg(ref) + def fetch(file, position, dets_symbol_table, dets_ref_table) do + ref = ReferenceTable.reference(dets_ref_table, file, position) query = case ref do diff --git a/lib/next_ls/hover.ex b/lib/next_ls/hover.ex new file mode 100644 index 00000000..8429c9cc --- /dev/null +++ b/lib/next_ls/hover.ex @@ -0,0 +1,207 @@ +defmodule NextLS.Hover do + alias GenLSP.Structures.{ + Hover, + MarkupContent, + Position, + Range + } + + alias NextLS.ReferenceTable + alias NextLS.Runtime + + @spec fetch(lsp :: GenLSP.LSP.t(), uri :: String.t(), position :: Position.t()) :: Hover.t() | nil + def fetch(lsp, uri, position) do + position = {position.line + 1, position.character + 1} + document = Enum.join(lsp.assigns.documents[uri], "\n") + + with {module, function, range} <- find_reference(lsp, document, uri, position), + docs when is_binary(docs) <- fetch_docs(lsp, document, module, function) do + %Hover{ + contents: %MarkupContent{ + kind: "markdown", + value: docs + }, + range: range + } + end + end + + defp find_reference(lsp, document, uri, position) do + surround_context = Code.Fragment.surround_context(document, position) + + if surround_context == :none do + nil + else + case ReferenceTable.reference(lsp.assigns.reference_table, URI.parse(uri).path, position) do + [%{type: :function, module: module, identifier: function} | _] -> + {module, function, build_range(surround_context)} + + [%{type: :alias, module: module} | _] -> + {module, nil, build_range(surround_context)} + + _other -> + find_in_context(surround_context) + end + end + end + + defp find_in_context(%{context: {:alias, module}} = context) do + {to_module(module), nil, build_range(context)} + end + + defp find_in_context(%{context: {:unquoted_atom, erlang_module}} = context) do + {to_atom(erlang_module), nil, build_range(context)} + end + + defp find_in_context(_context) do + nil + end + + defp fetch_docs(lsp, document, module, nil) do + with {:ok, {_, _, _, _, docs, _, _} = docs_v1} <- request_docs(lsp, document, module) do + print_doc(module, nil, nil, docs, docs_v1) + end + end + + defp fetch_docs(lsp, document, module, function) do + with {:ok, {_, _, _, _, _, _, functions_docs} = docs_v1} <- request_docs(lsp, document, module), + {_, _, [spec], function_docs, _} <- find_function_docs(functions_docs, function) do + print_doc(module, function, spec, function_docs, docs_v1) + end + end + + defp request_docs(lsp, document, module, attempt \\ 1) do + case send_fetch_docs_request(lsp, module) do + {:error, :not_ready} -> + nil + + {:ok, {:error, :module_not_found}} -> + if attempt < 2 do + aliased_module = find_aliased_module(document, module) + + if aliased_module do + request_docs(lsp, document, from_quoted_module_to_module(aliased_module), attempt + 1) + end + end + + {:ok, {:error, :chunk_not_found}} -> + nil + + other -> + other + end + end + + defp find_aliased_module(document, module) do + module = to_quoted_module(module) + + ast = + Code.string_to_quoted(document, + unescape: false, + token_metadata: true, + columns: true + ) + + {_ast, aliased_module} = + Macro.prewalk(ast, nil, fn + # alias A, as: B + {:alias, _, [{:__aliases__, _, aliased_module}, [as: {:__aliases__, _, ^module}]]} = expr, _ -> + {expr, aliased_module} + + # alias A.{B, C} + {:alias, _, [{{:., _, [{:__aliases__, _, namespace}, :{}]}, _, aliases}]} = expr, acc -> + aliases = Enum.map(aliases, fn {:__aliases__, _, md} -> md end) + + if module in aliases do + {expr, namespace ++ module} + else + {expr, acc} + end + + # alias A.B.C + {:alias, _, [{:__aliases__, _, aliased_module}]} = expr, acc -> + offset = length(aliased_module) - length(module) + + if Enum.slice(aliased_module, offset..-1) == module do + {expr, aliased_module} + else + {expr, acc} + end + + expr, acc -> + {expr, acc} + end) + + aliased_module + end + + defp send_fetch_docs_request(lsp, module) do + Runtime.call(lsp.assigns.runtime, {Code, :fetch_docs, [module]}) + end + + defp build_range(%{begin: {line, start}, end: {_, finish}}) do + %Range{ + start: %Position{line: line - 1, character: start - 1}, + end: %Position{line: line - 1, character: finish - 1} + } + end + + defp find_function_docs(docs, function) do + Enum.find(docs, fn + {{type, ^function, _}, _, _, _, _} when type in [:function, :macro] -> true + _ -> false + end) + end + + defp print_doc(_module, _function, _spec, :none, _docs_v1) do + nil + end + + defp print_doc(_module, _function, _spec, :hidden, _docs_v1) do + nil + end + + defp print_doc(_module, nil, _spec, doc, {_, _, _, "text/markdown", _, _, _}) do + doc["en"] + end + + defp print_doc(_module, _function, spec, doc, {_, _, _, "text/markdown", _, _, _}) do + print_function_spec(spec) <> doc["en"] + end + + defp print_doc(module, nil, _spec, _doc, {_, _, :erlang, "application/erlang+html", _, _, _} = docs_v1) do + :shell_docs.render(module, docs_v1, %{ansi: false}) |> Enum.join() + end + + defp print_doc(module, function, spec, _doc, {_, _, :erlang, "application/erlang+html", _, _, _} = docs_v1) do + print_function_spec(spec) <> (:shell_docs.render(module, function, docs_v1, %{ansi: false}) |> Enum.join()) + end + + defp print_function_spec(spec) do + "### " <> spec <> "\n\n" + end + + defp from_quoted_module_to_module(quoted_module) do + quoted_module + |> Enum.map(&Atom.to_string/1) + |> Enum.join(".") + |> to_charlist() + |> to_module() + end + + defp to_module(charlist) when is_list(charlist) do + String.to_atom("Elixir." <> to_string(charlist)) + end + + defp to_atom(charlist) when is_list(charlist) do + charlist |> to_string() |> String.to_atom() + end + + defp to_quoted_module(module) when is_atom(module) do + module + |> Atom.to_string() + |> String.replace("Elixir.", "") + |> String.split(".") + |> Enum.map(&String.to_atom/1) + end +end diff --git a/lib/next_ls/lsp_supervisor.ex b/lib/next_ls/lsp_supervisor.ex index 59bd7e66..c0c60b50 100644 --- a/lib/next_ls/lsp_supervisor.ex +++ b/lib/next_ls/lsp_supervisor.ex @@ -32,17 +32,21 @@ defmodule NextLS.LSPSupervisor do raise "Unknown option" end + path = Path.expand(".elixir-tools") + children = [ {DynamicSupervisor, name: NextLS.DynamicSupervisor}, {Task.Supervisor, name: NextLS.TaskSupervisor}, {Task.Supervisor, name: :runtime_task_supervisor}, {GenLSP.Buffer, buffer_opts}, {NextLS.DiagnosticCache, name: :diagnostic_cache}, - {NextLS.SymbolTable, name: :symbol_table, path: Path.expand(".elixir-tools")}, + {NextLS.SymbolTable, name: :symbol_table, path: path}, + {NextLS.ReferenceTable, name: :reference_table, path: path}, {Registry, name: NextLS.ExtensionRegistry, keys: :duplicate}, {NextLS, cache: :diagnostic_cache, symbol_table: :symbol_table, + reference_table: :reference_table, task_supervisor: NextLS.TaskSupervisor, runtime_task_supervisor: :runtime_task_supervisor, dynamic_supervisor: NextLS.DynamicSupervisor, diff --git a/lib/next_ls/reference_table.ex b/lib/next_ls/reference_table.ex new file mode 100644 index 00000000..2b272ca3 --- /dev/null +++ b/lib/next_ls/reference_table.ex @@ -0,0 +1,73 @@ +defmodule NextLS.ReferenceTable do + @moduledoc false + use GenServer + + def start_link(args) do + GenServer.start_link(__MODULE__, Keyword.take(args, [:path]), Keyword.take(args, [:name])) + end + + @spec reference(pid() | atom(), String.t(), {integer(), integer()}) :: list(struct()) + def reference(server, file, position), do: GenServer.call(server, {:reference, file, position}) + + @spec put_reference(pid() | atom(), map()) :: :ok + def put_reference(server, reference), do: GenServer.cast(server, {:put_reference, reference}) + + @spec close(pid() | atom()) :: :ok | {:error, term()} + def close(server), do: GenServer.call(server, :close) + + def init(args) do + path = Keyword.fetch!(args, :path) + reference_table_name = Keyword.get(args, :reference_table_name, :reference_table) + + File.mkdir_p!(path) + + {:ok, ref_name} = + :dets.open_file(reference_table_name, + file: path |> Path.join("reference_table.dets") |> String.to_charlist(), + type: :duplicate_bag + ) + + {:ok, %{table: ref_name}} + end + + def handle_call({:reference, file, {line, col}}, _, state) do + ref = + :dets.select( + state.table, + [ + {{{:"$1", {{:"$2", :"$3"}, {:"$4", :"$5"}}}, :"$6"}, + [ + {:andalso, + {:andalso, {:andalso, {:andalso, {:==, :"$1", file}, {:"=<", :"$2", line}}, {:"=<", :"$3", col}}, + {:"=<", line, :"$4"}}, {:"=<", col, :"$5"}} + ], [:"$6"]} + ] + ) + + {:reply, ref, state} + end + + def handle_call(:close, _, state) do + :dets.close(state.table) + + {:reply, :ok, state} + end + + def handle_cast({:put_reference, reference}, state) do + %{ + meta: meta, + identifier: identifier, + file: file + } = reference + + col = meta[:column] || 0 + + identifier_length = identifier |> to_string() |> String.replace("Elixir.", "") |> String.length() + + range = {{meta[:line], col}, {meta[:line], col + identifier_length}} + + :dets.insert(state.table, {{file, range}, reference}) + + {:noreply, state} + end +end diff --git a/priv/monkey/_next_ls_private_compiler.ex b/priv/monkey/_next_ls_private_compiler.ex index babbfb7f..b9a101c2 100644 --- a/priv/monkey/_next_ls_private_compiler.ex +++ b/priv/monkey/_next_ls_private_compiler.ex @@ -26,7 +26,7 @@ defmodule NextLSPrivate.Tracer do def trace({type, meta, module, func, arity}, env) when type in [:remote_function, :remote_macro, :imported_macro] and - module not in [:elixir_def, :elixir_utils, Kernel, Enum] do + module not in [:elixir_def, :elixir_utils, :elixir_module] do parent = parent_pid() if type == :remote_macro && meta[:closing][:line] != meta[:line] do diff --git a/test/next_ls_test.exs b/test/next_ls_test.exs index fcf16755..328b07ba 100644 --- a/test/next_ls_test.exs +++ b/test/next_ls_test.exs @@ -866,6 +866,479 @@ defmodule NextLSTest do end end + describe "hover language feature" do + # https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_hover + setup %{cwd: cwd} do + File.mkdir_p!(Path.join([cwd, "lib", "bar"])) + baz = Path.join(cwd, "lib/bar/baz.ex") + + File.write!(baz, """ + defmodule Bar.Baz do + @moduledoc "Bar.Baz module" + + @doc "Bar.Baz.q function" + def q do + "q" + end + end + """) + + fiz = Path.join(cwd, "lib/bar/fiz.ex") + + File.write!(fiz, """ + defmodule Bar.Fiz do + # No doc + def q do + "q" + end + end + """) + + guz = Path.join(cwd, "lib/bar/guz.ex") + + File.write!(guz, """ + defmodule Bar.Guz do + @moduledoc "Bar.Guz module" + + @doc "Bar.Guz.q function" + def q do + "q" + end + end + """) + + foo = Path.join(cwd, "lib/foo.ex") + + File.write!(foo, """ + defmodule Foo do + @moduledoc "Foo module" + + @doc "Foo.bar function" + def bar do + "baz" + end + end + """) + + example = Path.join(cwd, "lib/example.ex") + + File.write!(example, """ + defmodule Example do + @moduledoc "Example doc" + alias Foo, as: Foz + + alias Bar.{ + Fiz, + Baz + } + + alias Bar.Guz + + defstruct [:foo] + + def test do + q1 = Atom.to_string(:atom) + q2 = Foz.bar() + q3 = Baz.q() + q4 = Fiz.q() + q5 = Guz.q() + q6 = to_string(:abs) + :timer.sleep(1) + q7 = %Example{foo: "a"} + + + [q1] ++ [q2] ++ [q3] ++ [q4] ++ [q5] ++ [q6] ++ [q7.foo] + end + end + """) + + File.rm_rf!(Path.join(cwd, ".elixir-tools")) + + [example: example] + end + + setup :with_lsp + + test "gets module or function doc when hovering", %{client: client, example: example} do + assert :ok == notify(client, %{method: "initialized", jsonrpc: "2.0", params: %{}}) + assert_notification "window/logMessage", %{"message" => "[NextLS] Runtime ready..."} + assert_notification "window/logMessage", %{"message" => "[NextLS] Compiled!"} + + example_uri = uri(example) + + notify client, %{ + method: "textDocument/didOpen", + jsonrpc: "2.0", + params: %{ + textDocument: %{ + uri: example_uri, + languageId: "elixir", + version: 1, + text: File.read!(example) + } + } + } + + # 1. `defmodule` macro + + request client, %{ + method: "textDocument/hover", + id: 1, + jsonrpc: "2.0", + params: %{ + position: %{line: 0, character: 10}, + textDocument: %{uri: example_uri} + } + } + + assert_result 1, + %{ + "contents" => %{ + "kind" => "markdown", + "value" => "Example doc" + }, + "range" => %{ + "start" => %{"character" => 10, "line" => 0}, + "end" => %{"character" => 17, "line" => 0} + } + }, + 500 + + # 2. `alias` macro with :as option + + request client, %{ + method: "textDocument/hover", + id: 2, + jsonrpc: "2.0", + params: %{ + position: %{line: 2, character: 8}, + textDocument: %{uri: example_uri} + } + } + + assert_result 2, + %{ + "contents" => %{ + "kind" => "markdown", + "value" => "Foo module" + }, + "range" => %{ + "start" => %{"character" => 8, "line" => 2}, + "end" => %{"character" => 11, "line" => 2} + } + }, + 500 + + # 3. `alias` macro with :as option + + request client, %{ + method: "textDocument/hover", + id: 3, + jsonrpc: "2.0", + params: %{ + position: %{line: 2, character: 17}, + textDocument: %{uri: example_uri} + } + } + + assert_result 3, + %{ + "contents" => %{ + "kind" => "markdown", + "value" => "Foo module" + }, + "range" => %{ + "start" => %{"character" => 17, "line" => 2}, + "end" => %{"character" => 20, "line" => 2} + } + }, + 500 + + # 4. Multi `alias` macro + + request client, %{ + method: "textDocument/hover", + id: 4, + jsonrpc: "2.0", + params: %{ + position: %{line: 6, character: 5}, + textDocument: %{uri: example_uri} + } + } + + assert_result 4, + %{ + "contents" => %{ + "kind" => "markdown", + "value" => "Bar.Baz module" + }, + "range" => %{ + "start" => %{"character" => 4, "line" => 6}, + "end" => %{"character" => 7, "line" => 6} + } + }, + 500 + + # 5. `alias` macro + + request client, %{ + method: "textDocument/hover", + id: 5, + jsonrpc: "2.0", + params: %{ + position: %{line: 9, character: 13}, + textDocument: %{uri: example_uri} + } + } + + assert_result 5, + %{ + "contents" => %{ + "kind" => "markdown", + "value" => "Bar.Guz module" + }, + "range" => %{ + "start" => %{"character" => 8, "line" => 9}, + "end" => %{"character" => 15, "line" => 9} + } + }, + 500 + + # 6. Module reference + + request client, %{ + method: "textDocument/hover", + id: 6, + jsonrpc: "2.0", + params: %{ + position: %{line: 14, character: 9}, + textDocument: %{uri: example_uri} + } + } + + assert_result 6, + %{ + "contents" => %{ + "kind" => "markdown", + "value" => "Atoms are constants" <> _ + }, + "range" => %{ + "start" => %{"character" => 9, "line" => 14}, + "end" => %{"character" => 13, "line" => 14} + } + }, + 500 + + # 7. Function reference + + request client, %{ + method: "textDocument/hover", + id: 7, + jsonrpc: "2.0", + params: %{ + position: %{line: 14, character: 22}, + textDocument: %{uri: example_uri} + } + } + + assert_result 7, + %{ + "contents" => %{ + "kind" => "markdown", + "value" => + "### atom_to_binary/1\n\n\n -spec atom_to_binary(Atom) -> binary() when Atom :: atom()" <> + _ + }, + "range" => %{ + "start" => %{"character" => 9, "line" => 14}, + "end" => %{"character" => 23, "line" => 14} + } + }, + 500 + + # 8. User-defined module reference + + request client, %{ + method: "textDocument/hover", + id: 8, + jsonrpc: "2.0", + params: %{ + position: %{line: 16, character: 13}, + textDocument: %{uri: example_uri} + } + } + + assert_result 8, + %{ + "contents" => %{ + "kind" => "markdown", + "value" => "### q()\n\nBar.Baz.q function" + }, + "range" => %{ + "start" => %{"character" => 9, "line" => 16}, + "end" => %{"character" => 14, "line" => 16} + } + }, + 500 + + # 9. User-defined module reference without doc + + request client, %{ + method: "textDocument/hover", + id: 9, + jsonrpc: "2.0", + params: %{ + position: %{line: 17, character: 11}, + textDocument: %{uri: example_uri} + } + } + + assert_result 9, nil, 500 + + # 10. User-defined function reference without doc + + request client, %{ + method: "textDocument/hover", + id: 10, + jsonrpc: "2.0", + params: %{ + position: %{line: 17, character: 13}, + textDocument: %{uri: example_uri} + } + } + + assert_result 10, nil, 500 + + # 11. Kernel function + + request client, %{ + method: "textDocument/hover", + id: 11, + jsonrpc: "2.0", + params: %{ + position: %{line: 19, character: 13}, + textDocument: %{uri: example_uri} + } + } + + assert_result 11, + %{ + "contents" => %{ + "kind" => "markdown", + "value" => "### to_string(term)\n\nConverts the argument to a string" <> _ + }, + "range" => %{ + "start" => %{"character" => 9, "line" => 19}, + "end" => %{"character" => 18, "line" => 19} + } + }, + 500 + + # 12. Erlang module reference + + request client, %{ + method: "textDocument/hover", + id: 12, + jsonrpc: "2.0", + params: %{ + position: %{line: 20, character: 7}, + textDocument: %{uri: example_uri} + } + } + + assert_result 12, + %{ + "contents" => %{ + "kind" => "markdown", + "value" => "\n\ttimer\n\n This module provides useful functions related to time" <> _ + }, + "range" => %{ + "start" => %{"character" => 4, "line" => 20}, + "end" => %{"character" => 10, "line" => 20} + } + }, + 500 + + # 13. Erlang function reference + + request client, %{ + method: "textDocument/hover", + id: 13, + jsonrpc: "2.0", + params: %{ + position: %{line: 20, character: 13}, + textDocument: %{uri: example_uri} + } + } + + assert_result 13, + %{ + "contents" => %{ + "kind" => "markdown", + "value" => + "### sleep/1\n\n\n -spec sleep(Time) -> ok when Time :: timeout().\n\n Suspends the process" <> + _ + }, + "range" => %{ + "start" => %{"character" => 4, "line" => 20}, + "end" => %{"character" => 16, "line" => 20} + } + }, + 500 + + # 14. Struct reference + + request client, %{ + method: "textDocument/hover", + id: 14, + jsonrpc: "2.0", + params: %{ + position: %{line: 21, character: 13}, + textDocument: %{uri: example_uri} + } + } + + assert_result 14, + %{ + "contents" => %{ + "kind" => "markdown", + "value" => "Example doc" + }, + "range" => %{ + "start" => %{"character" => 9, "line" => 21}, + "end" => %{"character" => 17, "line" => 21} + } + }, + 500 + + # 15. Macro + + request client, %{ + method: "textDocument/hover", + id: 15, + jsonrpc: "2.0", + params: %{ + position: %{line: 13, character: 3}, + textDocument: %{uri: example_uri} + } + } + + assert_result 15, + %{ + "contents" => %{ + "kind" => "markdown", + "value" => + "### def(call, expr \\\\ nil)\n\nDefines a public function with the given name and body" <> _ + }, + "range" => %{ + "start" => %{"character" => 2, "line" => 13}, + "end" => %{"character" => 5, "line" => 13} + } + }, + 500 + end + end + defp with_lsp(%{tmp_dir: tmp_dir}) do root_path = Path.absname(tmp_dir) @@ -876,6 +1349,7 @@ defmodule NextLSTest do extensions = [NextLS.ElixirExtension] cache = start_supervised!(NextLS.DiagnosticCache) symbol_table = start_supervised!({NextLS.SymbolTable, path: tmp_dir}) + reference_table = start_supervised!({NextLS.ReferenceTable, path: tmp_dir}) server = server(NextLS, @@ -885,7 +1359,8 @@ defmodule NextLSTest do extension_registry: Registry.NextLSTest, extensions: extensions, cache: cache, - symbol_table: symbol_table + symbol_table: symbol_table, + reference_table: reference_table ) Process.link(server.lsp)